diff --git a/.env.docker.example b/.env.docker.example index e1606ad..93d1c11 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -14,3 +14,25 @@ MYSQL_USE_SSL=true # Redis 由 docker-compose 内 rlz-redis 提供,一般无需改 # REDIS_HOST=redis + +# ============ 安全配置 ============ +# JWT Token 签名密钥(务必使用强随机字符串,建议 openssl rand -hex 32) +TOKEN_SECRET=change_me_to_random_64chars + +# Druid 监控面板(生产环境保持 false) +DRUID_MONITOR_ENABLED=false +DRUID_ALLOW_IP=127.0.0.1 +DRUID_PASSWORD= + +# ============ 微信支付配置 ============ +WX_APP_ID=wx2d5ca40a16ebd1b3 +WX_SECRET= +WX_MCH_ID=1637125805 +WX_MCH_SERIAL_NO= +WX_API_V3_KEY= +WX_PRIVATE_KEY= +WX_NOTIFY_URL=https://ruilaizipj.com/prod-api/system/weixin/wxPayNotify +WX_REFUND_NOTIFY_URL=https://ruilaizipj.com/prod-api/system/weixin/wxRefundNotify + +# AES 加密密钥(用于 authToken 生成) +AES_ENCRYPT_KEY= diff --git a/coupon/app.js b/coupon/app.js index d5e906d..8789b6a 100644 --- a/coupon/app.js +++ b/coupon/app.js @@ -5,12 +5,16 @@ App({ onLaunch() { const uid = wx.getStorageSync('uid') const token = wx.getStorageSync('token') + const userType = wx.getStorageSync('userType') if (uid) { this.globalData.uid = uid } if (token) { this.globalData.token = token } + if (userType) { + this.globalData.userType = userType + } }, globalData: { userInfo: null, @@ -25,9 +29,64 @@ App({ phone: '', uid: '', + userType: 'C', // 当前角色: C=患者 B=陪诊师 06=推广员 05=调度 04=客服 03=运营 00=超管 sessionKey: '', version_number: '1.0.0', + // 角色 TabBar 配置 + ROLE_TABS: { + 'C': [ + { text: '首页', icon: '/pages/image/income.png', selIcon: '/pages/image/income_a.png' }, + { text: '服务', icon: '/pages/image/service.png', selIcon: '/pages/image/service.png' }, + { text: '订单', icon: '/pages/image/dingdan.png', selIcon: '/pages/image/dingdanl.png' }, + { text: '我的', icon: '/pages/image/my.png', selIcon: '/pages/image/my_a.png' } + ], + 'B': [ + { text: '待接单', icon: '/pages/image/income.png', selIcon: '/pages/image/income_a.png' }, + { text: '进行中', icon: '/pages/image/service.png', selIcon: '/pages/image/service.png' }, + { text: '已完成', icon: '/pages/image/dingdan.png', selIcon: '/pages/image/dingdanl.png' }, + { text: '我的', icon: '/pages/image/my.png', selIcon: '/pages/image/my_a.png' } + ], + '06': [ + { text: '推广数据', icon: '/pages/image/income.png', selIcon: '/pages/image/income_a.png' }, + { text: '我的团队', icon: '/pages/image/service.png', selIcon: '/pages/image/service.png' }, + { text: '佣金', icon: '/pages/image/dingdan.png', selIcon: '/pages/image/dingdanl.png' }, + { text: '我的', icon: '/pages/image/my.png', selIcon: '/pages/image/my_a.png' } + ], + '03': [ + { text: '待处理', icon: '/pages/image/income.png', selIcon: '/pages/image/income_a.png' }, + { text: '全部订单', icon: '/pages/image/dingdan.png', selIcon: '/pages/image/dingdanl.png' }, + { text: '收益', icon: '/pages/image/service.png', selIcon: '/pages/image/service.png' }, + { text: '我的', icon: '/pages/image/my.png', selIcon: '/pages/image/my_a.png' } + ], + '04': [ + { text: '待处理', icon: '/pages/image/income.png', selIcon: '/pages/image/income_a.png' }, + { text: '全部订单', icon: '/pages/image/dingdan.png', selIcon: '/pages/image/dingdanl.png' }, + { text: '收益', icon: '/pages/image/service.png', selIcon: '/pages/image/service.png' }, + { text: '我的', icon: '/pages/image/my.png', selIcon: '/pages/image/my_a.png' } + ], + '05': [ + { text: '待派单', icon: '/pages/image/income.png', selIcon: '/pages/image/income_a.png' }, + { text: '全部订单', icon: '/pages/image/dingdan.png', selIcon: '/pages/image/dingdanl.png' }, + { text: '收益', icon: '/pages/image/service.png', selIcon: '/pages/image/service.png' }, + { text: '我的', icon: '/pages/image/my.png', selIcon: '/pages/image/my_a.png' } + ] + }, + + // 角色中文名映射 + ROLE_LABELS: { + '00': '超级管理员', + '03': '运营经理', + '04': '客服专员', + '05': '调度员', + '06': '推广员', + 'B': '陪诊师', + 'C': '患者' + }, + + // 可自助切换的角色对 + ROLE_SWITCHABLE: ['C', '06', 'B'], + // 订单状态映射(订单状态:-2:已取消 -1:拒绝接单 0:待接单 1:已接单 // 2:待服务 3:服务中 4:已完成 5:退款中 6:退款中 7:已退款 8:已结算) STATUS_MAP: { @@ -100,7 +159,7 @@ App({ }, // 公共导航栏高度,避免每个页面重复计算 getNavBarHeight: function () { - var statusBarHeight = wx.getSystemInfoSync().statusBarHeight; + var statusBarHeight = (wx.getWindowInfo && wx.getWindowInfo().statusBarHeight) || wx.getSystemInfoSync().statusBarHeight; return { statusBarHeight: statusBarHeight, height: 46 + statusBarHeight diff --git a/coupon/app.json b/coupon/app.json index a4dd2c9..02c828a 100644 --- a/coupon/app.json +++ b/coupon/app.json @@ -33,36 +33,59 @@ "pages/payment/payment" ], "independent": false + }, + { + "root": "subPackages/pages-promoter", + "name": "pagesPromoter", + "pages": [ + "dashboard/dashboard", + "team/team", + "commission/commission", + "share/share" + ], + "independent": false + }, + { + "root": "subPackages/pages-work", + "name": "pagesWork", + "pages": [ + "dispatch/dispatch", + "refund/refund", + "ops/ops", + "search/search" + ], + "independent": false } ], "style": "v2", "sitemapLocation": "sitemap.json", "tabBar": { + "custom": true, "color": "#C1C1C1", "selectedColor": "#606060", "backgroundColor": "#fff", "list": [ { "pagePath": "pages/index/index", - "text": "首页", + "text": "Tab1", "iconPath": "pages/image/income.png", "selectedIconPath": "pages/image/income_a.png" }, { "pagePath": "pages/service/service", - "text": "服务", + "text": "Tab2", "iconPath": "pages/image/service.png", "selectedIconPath": "pages/image/service.png" }, { "pagePath": "pages/order/order", - "text": "订单", + "text": "Tab3", "iconPath": "pages/image/dingdan.png", "selectedIconPath": "pages/image/dingdanl.png" }, { "pagePath": "pages/my/my", - "text": "我的", + "text": "Tab4", "iconPath": "pages/image/my.png", "selectedIconPath": "pages/image/my_a.png" } diff --git a/coupon/custom-tab-bar/index.js b/coupon/custom-tab-bar/index.js new file mode 100644 index 0000000..c06febe --- /dev/null +++ b/coupon/custom-tab-bar/index.js @@ -0,0 +1,39 @@ +var app = getApp(); + +Component({ + data: { + selected: 0, + tabs: [] + }, + + lifetimes: { + attached() { + this._lastRole = ''; + this.updateTabs(); + } + }, + + pageLifetimes: { + show() { + this.updateTabs(); + } + }, + + methods: { + // 仅当角色实际变化时才更新 tabs,避免无意义重渲染 + updateTabs() { + var userType = app.globalData.userType || 'C'; + if (this._lastRole === userType) return; + this._lastRole = userType; + var tabs = app.globalData.ROLE_TABS[userType] || app.globalData.ROLE_TABS['C']; + this.setData({ tabs: tabs }); + }, + + switchTab(e) { + var index = e.currentTarget.dataset.index; + var pages = ['/pages/index/index', '/pages/service/service', '/pages/order/order', '/pages/my/my']; + if (this.data.selected === index) return; + wx.switchTab({ url: pages[index] }); + } + } +}); diff --git a/coupon/custom-tab-bar/index.json b/coupon/custom-tab-bar/index.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/coupon/custom-tab-bar/index.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/coupon/custom-tab-bar/index.wxml b/coupon/custom-tab-bar/index.wxml new file mode 100644 index 0000000..3009bb7 --- /dev/null +++ b/coupon/custom-tab-bar/index.wxml @@ -0,0 +1,15 @@ + + + + {{item.text}} + + diff --git a/coupon/custom-tab-bar/index.wxss b/coupon/custom-tab-bar/index.wxss new file mode 100644 index 0000000..a10266d --- /dev/null +++ b/coupon/custom-tab-bar/index.wxss @@ -0,0 +1,35 @@ +.tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 98rpx; + background: #fff; + display: flex; + flex-direction: row; + border-top: 1rpx solid #eee; + padding-bottom: env(safe-area-inset-bottom); +} + +.tab-item { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.tab-icon { + width: 44rpx; + height: 44rpx; +} + +.tab-text { + font-size: 20rpx; + color: #C1C1C1; + margin-top: 4rpx; +} + +.tab-text.active { + color: #606060; +} diff --git a/coupon/pages/index/index.js b/coupon/pages/index/index.js index 61de52c..c84f6be 100644 --- a/coupon/pages/index/index.js +++ b/coupon/pages/index/index.js @@ -1,4 +1,4 @@ -// 服务图标名称 -> 本地 PNG 文件名的映射(以防 DB icon 字段为空或名称不匹配) +// 服务图标名称 -> 本地 PNG 文件名的映射 var iconNameMap = { '全面陪诊': '全程陪诊', '提前约号': '诊前约号', @@ -23,64 +23,205 @@ var app = getApp(); const { request } = require('../../utils/request') Page({ - data: { + // Patient mode hospitalData: [], - bannerdata: [ - "home-banner", - "home-banner", - "home-banner" - ], + bannerdata: ["home-banner", "home-banner", "home-banner"], itemView: [], - firstLoad: true + firstLoad: true, + // Caregiver mode + userType: '', + cgOrders: [], + cgPageNum: 1, + cgHasMore: true, + cgLoading: false, + // Promoter mode + proStats: { inviteCount: 0, teamCount: 0, totalCommission: '0', settledCommission: '0', pendingCommission: '0', orderCount: 0, promotionCode: '' }, + proLoading: false, + // Work mode (dispatcher/CS/ops) + workOrders: [], + workLoading: false }, - /** - * 诊前约号、代办买药、取送结果、出入院办理、就医陪诊、园内护理 - */ - itemClick(options) { + onLoad: function(options) { + // Check for promotion code from share link + if (options && options.promotionCode) { + this.bindPromotion(options.promotionCode); + } + }, - if (wx.getStorageSync('phone') == '') { - wx.setStorageSync('uidFlag', false); - console.log('请登录') - this.isdenglu() + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 0 }); + } + var userType = app.globalData.userType || 'C'; + var roleChanged = this.data.userType !== userType; + this.setData({ userType: userType }); + if (this.data.firstLoad || roleChanged) { + this.setData({ firstLoad: false }); + this.loadRoleContent(); + } + }, + + onPullDownRefresh() { + this.loadRoleContent().finally(function() { wx.stopPullDownRefresh(); }); + }, + + onReachBottom() { + if (this.data.userType === 'B' && this.data.cgHasMore && !this.data.cgLoading) { + this.loadCgOrders(); + } + }, + + // Load content based on role + loadRoleContent() { + var ut = this.data.userType; + if (ut === 'B') { + this.loadCgOrders(); + } else if (ut === '06') { + this.loadPromoterStats(); + } else if (ut === '03' || ut === '04' || ut === '05') { + this.loadWorkPending(); } else { - let item = JSON.stringify(options.currentTarget.dataset.item); - wx.navigateTo({ - // url: "/pages/peizhen/peizhendetail?servername=" + options.currentTarget.dataset.id, - url: "/pages/peizhen/peizhendetail?item=" + item, + this.getServiceTypes(); + this.getHospitalList(); + } + }, + + // === Promoter: Load dashboard stats === + async loadPromoterStats() { + this.setData({ proLoading: true }); + try { + var res = await request({ url: '/system/promotion/myStats', method: 'GET', loading: false }); + if (res.code === 200 && res.data) { + this.setData({ proStats: res.data }); + } + } catch (e) {} + this.setData({ proLoading: false }); + }, + + goProTeam() { wx.navigateTo({ url: '/subPackages/pages-promoter/team/team' }); }, + goProCommission() { wx.navigateTo({ url: '/subPackages/pages-promoter/commission/commission' }); }, + goProShare() { wx.navigateTo({ url: '/subPackages/pages-promoter/share/share' }); }, + + // === Work mode: Load pending tasks (03/04/05) === + async loadWorkPending() { + this.setData({ workLoading: true }); + try { + // Load status=0 (pending) orders for dispatchers, status=5 (refund) for CS + var status = this.data.userType === '04' ? '5' : '0'; + var res = await request({ + url: '/system/view/list', + method: 'GET', + data: { pageNum: 1, pageSize: 10, status: status }, + loading: false }); - } + if (res.code === 200) { + this.setData({ workOrders: (res.rows || res.data || []) }); + } + } catch (e) {} + this.setData({ workLoading: false }); }, - goService() { - wx.switchTab({ url: '/pages/service/service' }) + goWorkDispatch() { wx.navigateTo({ url: '/subPackages/pages-work/dispatch/dispatch' }); }, + goWorkRefund() { wx.navigateTo({ url: '/subPackages/pages-work/refund/refund' }); }, + goWorkSearch() { wx.navigateTo({ url: '/subPackages/pages-work/search/search' }); }, + goWorkOps() { wx.navigateTo({ url: '/subPackages/pages-work/ops/ops' }); }, + + // === Bind promotion code from share link === + bindPromotion(code) { + // Store promotion code for use during registration + wx.setStorageSync('promotionCode', code); }, - go_peizhen() { - if (wx.getStorageSync('phone') == '') { - wx.setStorageSync('uidFlag', false); - console.log('请登录') - this.isdenglu() - } else { - wx.navigateTo({ - url: '/pages/peizhen/peizhen', - }) - } + // === Caregiver: Load pending orders (status=0) === + async loadCgOrders() { + if (this.data.cgLoading) return; + this.setData({ cgLoading: true }); + try { + var res = await request({ + url: '/system/view/list', + method: 'GET', + data: { pageNum: this.data.cgPageNum, pageSize: 20, status: '0' }, + loading: false + }); + if (res.code === 200) { + var list = res.rows || res.data || []; + if (this.data.cgPageNum === 1) { + this.setData({ cgOrders: list, cgHasMore: list.length >= 20, cgPageNum: 2 }); + } else { + this.setData({ + cgOrders: this.data.cgOrders.concat(list), + cgHasMore: list.length >= 20, + cgPageNum: this.data.cgPageNum + 1 + }); + } + } + } catch (e) {} + this.setData({ cgLoading: false }); }, - go_peihu() { - if (wx.getStorageSync('phone') == '') { - wx.setStorageSync('uidFlag', false); - console.log('请登录') - this.isdenglu() - } else { - wx.navigateTo({ - url: '/pages/peihu/peihu?' + "hospitalId=" + "", - }) - } + // === Caregiver: Refuse order === + refuseOrder(e) { + var item = e.currentTarget.dataset.item; + var that = this; + wx.showModal({ + title: '确认拒单', + content: '确定拒绝订单 ' + (item.yuliu1 || '#' + item.orderId) + ' ?', + success: function(res) { + if (res.confirm) that.doRefuse(item); + } + }); }, - //获取服务类型列表(从后台动态加载,最多显示8个热门推荐) + + async doRefuse(item) { + try { + var res = await request({ + url: '/system/view/refuseOrder', + method: 'POST', + data: { orderId: item.orderId }, + loading: true, + loadingText: '处理中...' + }); + if (res.code === 200) { + wx.showToast({ title: '已拒绝', icon: 'success' }); + var orders = this.data.cgOrders.filter(function(o) { return o.orderId !== item.orderId; }); + this.setData({ cgOrders: orders }); + } + } catch (e) {} + }, + + // === Caregiver: Accept order === + acceptOrder(e) { + var item = e.currentTarget.dataset.item; + var that = this; + wx.showModal({ + title: '确认接单', + content: '确认接受订单 ' + (item.yuliu1 || '#' + item.orderId) + ' ?', + success: function(res) { + if (res.confirm) that.doAccept(item); + } + }); + }, + + async doAccept(item) { + try { + var res = await request({ + url: '/system/view/acceptOrderYes', + method: 'POST', + data: { orderId: item.orderId }, + loading: true, + loadingText: '接单中...' + }); + if (res.code === 200) { + wx.showToast({ title: '接单成功', icon: 'success' }); + var orders = this.data.cgOrders.filter(function(o) { return o.orderId !== item.orderId; }); + this.setData({ cgOrders: orders }); + } + } catch (e) {} + }, + + // === Patient: service type list === getServiceTypes: function () { var that = this; request({ @@ -88,7 +229,7 @@ Page({ method: 'GET', data: { category: '2' }, loading: false - }).then(res => { + }).then(function(res) { var services = res.data || []; if (services.length) { var mapped = services.slice(0, 8).map(function (s) { @@ -99,22 +240,22 @@ Page({ describe: s.description || '', ordertype: s.typeCode } - }) - that.setData({ itemView: mapped }) + }); + that.setData({ itemView: mapped }); } }).catch(function() { - wx.showToast({ title: '加载服务失败', icon: 'none' }) - }) + wx.showToast({ title: '加载服务失败', icon: 'none' }); + }); }, - //获取医院列表 + // === Patient: hospital list === getHospitalList: function () { var that = this; request({ url: '/system/hospital/list', method: 'GET', loading: false - }).then(res => { + }).then(function(res) { var hospitals = res.rows || res.data || []; if (hospitals.length) { var mapped = hospitals.map(function (h) { @@ -127,91 +268,67 @@ Page({ skiled: h.hospitalDesc || '', address: h.address || '', img: imgUrl - } - }) - that.setData({ hospitalData: mapped }) + }; + }); + that.setData({ hospitalData: mapped }); } }).catch(function() { - wx.showToast({ title: '加载医院失败', icon: 'none' }) - }) + wx.showToast({ title: '加载医院失败', icon: 'none' }); + }); }, - /** - * 医院列表 - */ - listItemTap(options) { - // console.log(options.currentTarget.dataset.id), + + // === Shared: item click handlers === + itemClick(options) { if (wx.getStorageSync('phone') == '') { wx.setStorageSync('uidFlag', false); - console.log('请登录') - this.isdenglu() + this.isdenglu(); } else { - wx.navigateTo({ - url: '/pages/peihu/peihu?' + "hospitalId=" + options.currentTarget.dataset.id, - }) + var item = JSON.stringify(options.currentTarget.dataset.item); + wx.navigateTo({ url: "/pages/peizhen/peizhendetail?item=" + item }); + } + }, + + goService() { + wx.switchTab({ url: '/pages/service/service' }); + }, + + go_peizhen() { + if (wx.getStorageSync('phone') == '') { + wx.setStorageSync('uidFlag', false); + this.isdenglu(); + } else { + wx.navigateTo({ url: '/pages/peizhen/peizhen' }); + } + }, + + go_peihu() { + if (wx.getStorageSync('phone') == '') { + wx.setStorageSync('uidFlag', false); + this.isdenglu(); + } else { + wx.navigateTo({ url: '/pages/peihu/peihu?hospitalId=' }); + } + }, + + listItemTap(options) { + if (wx.getStorageSync('phone') == '') { + wx.setStorageSync('uidFlag', false); + this.isdenglu(); + } else { + wx.navigateTo({ url: '/pages/peihu/peihu?hospitalId=' + options.currentTarget.dataset.id }); } }, - //是否登录 isdenglu() { wx.showModal({ title: '提示', content: '请先登录', - success(res) { - if (res.confirm) { - console.log('用户点击确定') - wx.switchTab({ - url: '/pages/my/my' - }) - } else if (res.cancel) { - console.log('用户点击取消') - } + success: function(res) { + if (res.confirm) wx.switchTab({ url: '/pages/my/my' }); } - }) + }); }, - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - if (this.data.firstLoad) { - this.setData({ firstLoad: false }) - this.getServiceTypes() - this.getHospitalList() - } - }, - - onPullDownRefresh() { - this.getServiceTypes() - this.getHospitalList() - wx.stopPullDownRefresh() - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file + onReady: function() {}, + onShareAppMessage: function() {} +}); diff --git a/coupon/pages/index/index.wxml b/coupon/pages/index/index.wxml index 1f79d15..475dc31 100644 --- a/coupon/pages/index/index.wxml +++ b/coupon/pages/index/index.wxml @@ -1,4 +1,105 @@ - + + + + + + + + + {{proStats.inviteCount || 0}} + 直接邀请 + + + {{proStats.teamCount || 0}} + 团队总人数 + + + ¥{{proStats.totalCommission || '0'}} + 佣金总额 + + + {{proStats.orderCount || 0}} + 推广订单 + + + + + 👥 我的团队 + + + 💰 佣金明细 + + + 📤 分享推广 + + + 加载中... + + + + + + {{userType === '05' ? '待派单' : userType === '04' ? '退款申请' : '待处理'}} + + + + + {{item.yuliu1 || '订单#' + item.orderId}} + ¥{{item.yuguMoney || item.orderMoney}} + + + {{item.createTime}} + {{item.status === '0' ? '待接单' : item.status === '5' ? '退款中' : ''}} + + + + 暂无待处理项 + 加载中... + + + 📋 全部派单 + + + 📋 退款审核 + + + 📊 运营看板 + + + 🔍 订单搜索 + + + + + + + + 待接单 + 可接的陪诊/陪护订单 + + + + + {{item.yuliu1 || '订单#' + item.orderId}} + ¥{{item.yuguMoney || item.orderMoney}} + + + {{item.hospitalName || ''}} | {{item.serviceName || item.yuliu2 || ''}} + + + {{item.createTime}} + 接单 + 拒单 + + + + 暂无待接订单 + 加载中... + — 已加载全部 — + + + + @@ -20,7 +121,7 @@ - + {{item.title}} @@ -70,4 +171,4 @@ - \ No newline at end of file + diff --git a/coupon/pages/index/index.wxss b/coupon/pages/index/index.wxss index 700ce9e..dcd2893 100644 --- a/coupon/pages/index/index.wxss +++ b/coupon/pages/index/index.wxss @@ -226,4 +226,42 @@ .section-more{ font-size: 24rpx; color: #999; -} \ No newline at end of file +} + +/* === Caregiver style === */ +.cg-container { min-height: 100vh; background: #F5F5F5; padding-bottom: 40rpx; } +.cg-header { background: #fff; padding: 32rpx 40rpx; margin-bottom: 16rpx; } +.cg-header-title { font-size: 36rpx; font-weight: bold; color: #333; display: block; } +.cg-header-sub { font-size: 24rpx; color: #999; margin-top: 8rpx; } +.cg-card { background: #fff; margin: 12rpx 0 0 0; padding: 24rpx 32rpx; } +.cg-row { display: flex; justify-content: space-between; align-items: center; } +.cg-row.sub { margin-top: 8rpx; } +.cg-order-no { font-size: 28rpx; font-weight: 600; color: #333; } +.cg-amount { font-size: 30rpx; font-weight: 700; color: #E6A23C; } +.cg-meta { font-size: 24rpx; color: #999; } +.cg-time { font-size: 22rpx; color: #C0C4CC; } +.cg-accept-btn { background: #1890FF; color: #fff; font-size: 24rpx; padding: 8rpx 28rpx; border-radius: 24rpx; } +.cg-refuse-btn { background: #fff; color: #999; font-size: 24rpx; padding: 8rpx 20rpx; border-radius: 24rpx; border: 1rpx solid #ddd; margin-right: 12rpx; } +.cg-btn-group { display: flex; align-items: center; } +.loading { text-align: center; padding: 40rpx; color: #999; } +.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 28rpx; } +.end { text-align: center; padding: 32rpx; color: #C0C4CC; font-size: 24rpx; } + +/* === Multi-role shared styles === */ +.container { padding: 0 0 40rpx 0; background: #F5F5F5; min-height: 100vh; } +.top-banner { background: linear-gradient(135deg, #FF6A6A, #FF8F1F); color: #fff; padding: 48rpx 32rpx 40rpx; } +.banner-title { font-size: 36rpx; font-weight: 800; } +.banner-code { font-size: 24rpx; margin-top: 8rpx; opacity: 0.85; } + +.stats-grid { display: flex; flex-wrap: wrap; margin: -20rpx 24rpx 0; } +.stat-card { width: 44%; margin: 8rpx 3%; background: #fff; border-radius: 16rpx; padding: 28rpx 24rpx; text-align: center; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); } +.stat-num { font-size: 40rpx; font-weight: 800; color: #333; } +.stat-num.gold { color: #FF8F1F; } +.stat-num.red { color: #F56C6C; } +.stat-label { font-size: 22rpx; color: #999; margin-top: 8rpx; } + +.action-grid { display: flex; margin: 24rpx 32rpx; } +.action-item { flex: 1; background: #fff; border-radius: 16rpx; padding: 32rpx 0; text-align: center; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); font-size: 26rpx; color: #333; } +.action-item + .action-item { margin-left: 16rpx; } + +.cg-status { font-size: 22rpx; padding: 4rpx 16rpx; border-radius: 16rpx; background: #409EFF; color: #fff; } \ No newline at end of file diff --git a/coupon/pages/mechanism/mechanism.wxml b/coupon/pages/mechanism/mechanism.wxml index 22dee3c..f85e116 100644 --- a/coupon/pages/mechanism/mechanism.wxml +++ b/coupon/pages/mechanism/mechanism.wxml @@ -19,7 +19,7 @@ - + diff --git a/coupon/pages/my/my.js b/coupon/pages/my/my.js index 25b8121..9067776 100644 --- a/coupon/pages/my/my.js +++ b/coupon/pages/my/my.js @@ -11,7 +11,11 @@ Page({ userInfo: null, balance: '0.00', isAuth: false, - authName: '' + authName: '', + userType: 'C', + roleLabel: '患者', + // 首次登录身份选择 + showIdentityPicker: false }, onLoad: function () { @@ -19,6 +23,10 @@ Page({ }, onShow: function () { + // 更新自定义 TabBar 选中态并刷新 tabs(角色可能已切换) + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 3 }); + } if (!this.data.uidFlag) { this.getLogin() } else { @@ -53,7 +61,23 @@ Page({ isAuth: d.attestation === '1', authName: d.name || '' }); - // 同步头像/昵称到缓存 + // 同步角色信息:仅首次登录或后端角色与本地一致时同步 + // 若用户通过"切换"手动选择了角色,则保留用户选择不被后端覆盖 + var backendType = d.userType || 'C'; + var localType = wx.getStorageSync('userType'); + var switchable = app.globalData.ROLE_SWITCHABLE || ['C', '06', 'B']; + // 本地有手动切换且后端角色不同 → 保留本地 + if (localType && localType !== backendType && switchable.indexOf(localType) >= 0) { + var ut = localType; + } else { + var ut = backendType; + app.globalData.userType = ut; + wx.setStorageSync('userType', ut); + } + this.setData({ + userType: ut, + roleLabel: app.globalData.ROLE_LABELS[ut] || '患者' + }); if (d.nickName) wx.setStorageSync('nickName', d.nickName); } } catch (e) {} @@ -91,36 +115,62 @@ Page({ wx.showToast({ title: '登录成功', icon: 'success' }) - // 同步登录信息 - this.syncLogin() - // 拉取用户信息 - this.loadUserInfo() - - // 返回上一页,若无法返回则到首页 - var pages = getCurrentPages() - if (pages.length > 1) { - wx.navigateBack() + // 首次登录 → 弹出身份选择;已有身份 → 直接加载 + var existingType = wx.getStorageSync('userType'); + if (existingType) { + this.loadUserInfo(); + this._navigateAfterLogin(); } else { - wx.switchTab({ url: '/pages/index/index' }) + // 延迟弹出,等 toast 显示后 + var that = this; + setTimeout(function() { that.setData({ showIdentityPicker: true }); }, 800); } } catch (e) { this.setData({ logging: false }) } }, - // 同步登录信息 - syncLogin() { - request({ - url: '/member/logintwo', - method: 'POST', - loading: false, - showError: false, - data: { - uid: wx.getStorageSync('uid'), - phone: wx.getStorageSync('phone'), - token: wx.getStorageSync('token') - } - }) + _navigateAfterLogin() { + var pages = getCurrentPages(); + if (pages.length > 1) { + wx.navigateBack(); + } else { + wx.switchTab({ url: '/pages/index/index' }); + } + }, + + // 身份选择:点击选项 + selectIdentity(e) { + var role = e.currentTarget.dataset.role; + if (!role) return; + app.globalData.userType = role; + wx.setStorageSync('userType', role); + this.setData({ + userType: role, + roleLabel: app.globalData.ROLE_LABELS[role] || role, + showIdentityPicker: false + }); + // 尝试同步到后端 + this._syncRoleToBackend(role); + // 刷新 TabBar 和用户信息 + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().updateTabs(); + } + this.loadUserInfo(); + var that = this; + setTimeout(function() { that._navigateAfterLogin(); }, 300); + }, + + async _syncRoleToBackend(role) { + try { + await request({ + url: '/system/user/updateUserType', + method: 'PUT', + data: { userType: role }, + loading: false, + showError: false + }); + } catch (e) {} }, // 我的钱包 @@ -134,8 +184,50 @@ Page({ }, // 联系客服 - callService() { - wx.makePhoneCall({ phoneNumber: '4001234567' }) + async callService() { + try { + var res = await request({ + url: '/system/config/configKey/platform.servicePhone', + method: 'GET', + loading: false, + showError: false + }); + var phone = (res.code === 200 && res.data) ? res.data : '400-123-4567'; + wx.makePhoneCall({ phoneNumber: phone }); + } catch (e) { + wx.makePhoneCall({ phoneNumber: '400-123-4567' }); + } + }, + + // 角色切换(自助切换:患者/陪诊师/推广员) + showRoleSwitch() { + var that = this; + var currentRole = app.globalData.userType || 'C'; + var switchableList = app.globalData.ROLE_SWITCHABLE || ['C', '06', 'B']; + var items = switchableList.map(function(r) { + return (r === currentRole ? '✓ ' : '') + (app.globalData.ROLE_LABELS[r] || r); + }); + wx.showActionSheet({ + itemList: items, + success: function(res) { + var newRole = switchableList[res.tapIndex]; + if (newRole && newRole !== currentRole) { + app.globalData.userType = newRole; + wx.setStorageSync('userType', newRole); + that.setData({ + userType: newRole, + roleLabel: app.globalData.ROLE_LABELS[newRole] || newRole + }); + // 刷新 TabBar + if (typeof that.getTabBar === 'function' && that.getTabBar()) { + that.getTabBar().updateTabs(); + } + wx.showToast({ title: '已切换为' + (app.globalData.ROLE_LABELS[newRole] || newRole), icon: 'none' }); + // 跳回首页刷新 + setTimeout(function() { wx.switchTab({ url: '/pages/index/index' }); }, 500); + } + } + }); }, // 关于 diff --git a/coupon/pages/my/my.wxml b/coupon/pages/my/my.wxml index 6701486..1aee483 100644 --- a/coupon/pages/my/my.wxml +++ b/coupon/pages/my/my.wxml @@ -1,3 +1,28 @@ + + + + 选择您的身份 + 首次注册,请选择您需要的服务身份 + + + 🏥 + 我需要陪诊服务 + 浏览医院、下单预约陪诊 + + + 💼 + 我提供陪诊服务 + 接单赚钱,需实名认证 + + + 📣 + 我推广赚佣金 + 邀请好友下单,赚推广佣金 + + + + + + + + 当前身份: + {{roleLabel}} + 切换 > + + 账户余额(元) diff --git a/coupon/pages/my/my.wxss b/coupon/pages/my/my.wxss index 1c5a345..899649f 100644 --- a/coupon/pages/my/my.wxss +++ b/coupon/pages/my/my.wxss @@ -62,6 +62,38 @@ color: #FF9800; } +/* 角色标识 */ +.role-tag-row { + display: flex; + align-items: center; + margin: 0 32rpx 0 32rpx; + padding: 16rpx 24rpx; + background: #F0F7FF; + border-radius: 12rpx; + border: 1rpx solid #D6E9FF; +} + +.role-label { + font-size: 24rpx; + color: #666; +} + +.role-value { + font-size: 26rpx; + font-weight: 600; + color: #1890FF; + margin-left: 8rpx; +} + +.role-switch-btn { + margin-left: auto; + font-size: 22rpx; + color: #1890FF; + padding: 4rpx 12rpx; + border: 1rpx solid #1890FF; + border-radius: 8rpx; +} + /* 余额卡片 */ .balance-card { margin: 24rpx 32rpx; @@ -141,3 +173,78 @@ .login-btn::after { border: none; } + +/* 首次登录身份选择弹窗 */ +.identity-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.identity-dialog { + width: 620rpx; + background: #fff; + border-radius: 24rpx; + padding: 48rpx 40rpx 40rpx; + box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.15); +} + +.identity-title { + font-size: 36rpx; + font-weight: 700; + color: #333; + text-align: center; + margin-bottom: 12rpx; +} + +.identity-sub { + font-size: 24rpx; + color: #999; + text-align: center; + margin-bottom: 36rpx; +} + +.identity-options { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.identity-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 32rpx 24rpx; + border: 2rpx solid #E8E8E8; + border-radius: 16rpx; + transition: all 0.2s; +} + +.identity-option:active { + border-color: #1890FF; + background: #F0F7FF; +} + +.io-icon { + font-size: 48rpx; + margin-bottom: 12rpx; +} + +.io-name { + font-size: 30rpx; + font-weight: 600; + color: #333; + margin-bottom: 8rpx; +} + +.io-desc { + font-size: 22rpx; + color: #999; +} diff --git a/coupon/pages/myshoucang/myorderdetail.js b/coupon/pages/myshoucang/myorderdetail.js index 9242958..22de463 100644 --- a/coupon/pages/myshoucang/myorderdetail.js +++ b/coupon/pages/myshoucang/myorderdetail.js @@ -6,6 +6,7 @@ Page({ * 页面的初始数据 */ data: { + height: '', dingdan_msg:false, order_id:0, goods_id:'', @@ -54,6 +55,7 @@ Page({ * 生命周期函数--监听页面加载 */ onLoad: function (options) { + this.setData(getApp().getNavBarHeight()); console.log(options.type) console.log(options.id) if(options.type==2){ diff --git a/coupon/pages/myshoucang/myorderdetail.json b/coupon/pages/myshoucang/myorderdetail.json index 7249a3c..9d74f77 100644 --- a/coupon/pages/myshoucang/myorderdetail.json +++ b/coupon/pages/myshoucang/myorderdetail.json @@ -3,5 +3,6 @@ "van-nav-bar":"/miniprogram/miniprogram_npm/@vant/weapp/nav-bar/index", "van-uploader": "/miniprogram/miniprogram_npm/@vant/weapp/uploader/index" }, - "navigationBarTitleText": "订单详情" + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" } \ No newline at end of file diff --git a/coupon/pages/myshoucang/myorderdetail.wxml b/coupon/pages/myshoucang/myorderdetail.wxml index 5055215..7952868 100644 --- a/coupon/pages/myshoucang/myorderdetail.wxml +++ b/coupon/pages/myshoucang/myorderdetail.wxml @@ -1,4 +1,12 @@ + + @@ -60,8 +68,5 @@ - - - - + diff --git a/coupon/pages/myshoucang/myshoucang_index.js b/coupon/pages/myshoucang/myshoucang_index.js index de2ed82..7b2b1bd 100644 --- a/coupon/pages/myshoucang/myshoucang_index.js +++ b/coupon/pages/myshoucang/myshoucang_index.js @@ -6,6 +6,7 @@ Page({ * 页面的初始数据 */ data: { + height: '', resp:{ "erro": 0, "msg": "查询成功", @@ -52,44 +53,21 @@ Page({ * 生命周期函数--监听页面加载 */ onLoad: function (options) { + this.setData(getApp().getNavBarHeight()); this.setData({ type:options.type }) if(options.type==1){ - wx.setNavigationBarTitle({ - title: '我的关注' - }) + wx.setNavigationBarTitle({ title: '我的关注' }) this.getCollection(); } if(options.type==2){ - wx.setNavigationBarTitle({ - title: '我的拼团' - }) + wx.setNavigationBarTitle({ title: '我的拼团' }) this.getCollage() } if(options.type==3){ - wx.setNavigationBarTitle({ - title: '我的砍价' - }) + wx.setNavigationBarTitle({ title: '我的砍价' }) } - - this.setData({ - menuButtonInfo: wx.getMenuButtonBoundingClientRect() - }) - console.log(this.data.menuButtonInfo) - const { top, width, height, right } = this.data.menuButtonInfo - wx.getSystemInfo({ - success: (res) => { - const { statusBarHeight } = res - const margin = top - statusBarHeight - this.setData({ - navHeight: (height + statusBarHeight + (margin * 2) + 15), - searchMarginTop: statusBarHeight + margin, // 状态栏 + 胶囊按钮边距 - searchHeight: height, // 与胶囊按钮同高 - searchWidth: right - width // 胶囊按钮右边坐标 - 胶囊按钮宽度 = 按钮左边可使用宽度 - }) - }, - }) }, /** diff --git a/coupon/pages/myshoucang/myshoucang_index.json b/coupon/pages/myshoucang/myshoucang_index.json index 93a9bd2..b98fd12 100644 --- a/coupon/pages/myshoucang/myshoucang_index.json +++ b/coupon/pages/myshoucang/myshoucang_index.json @@ -1,4 +1,7 @@ { - "usingComponents": {}, - "navigationBarTitleText": "我的关注" + "usingComponents": { + "van-nav-bar":"/miniprogram/miniprogram_npm/@vant/weapp/nav-bar/index" + }, + "navigationBarTitleText": "我的关注", + "navigationStyle": "custom" } \ No newline at end of file diff --git a/coupon/pages/myshoucang/myshoucang_index.wxml b/coupon/pages/myshoucang/myshoucang_index.wxml index ec0e433..43537f2 100644 --- a/coupon/pages/myshoucang/myshoucang_index.wxml +++ b/coupon/pages/myshoucang/myshoucang_index.wxml @@ -1,6 +1,14 @@ - + + + - + @@ -22,4 +30,5 @@ + \ No newline at end of file diff --git a/coupon/pages/order/order.js b/coupon/pages/order/order.js index 72680e4..7efed9f 100644 --- a/coupon/pages/order/order.js +++ b/coupon/pages/order/order.js @@ -1,7 +1,6 @@ var app = getApp(); const { request } = require('../../utils/request') -// 处理订单列表中的图片路径 function processOrderList(list) { return list.map(function(item) { item.userbAvatar = app.fixImageUrl(item.userbAvatar) || '/pages/image/userimg.png'; @@ -11,131 +10,200 @@ function processOrderList(list) { Page({ data: { + // Patient mode order_list: [], nav_type: 1, all: true, height: '', url: app.globalData.url, - statusType: ["全部", "待接单", "已接单", "服务中", "待支付", "已完成","已取消"], - status: ["", "0", "1", "2", "3","4","-2"], + statusType: ["全部", "待接单", "已接单", "服务中", "待支付", "已完成", "已取消", "申请退款", "已退款", "已结算"], + status: ["", "0", "1", "2", "3", "4", "-2", "5", "7", "8"], currentType: 0, tabClass: ["", "", "", "", "", "", ""], pageNum: 1, hasMore: true, firstLoad: true, - loading: false - }, - - statusTap: function (e) { - var curType = e.currentTarget.dataset.index; - this.setData({ - currentType: curType, - pageNum: 1, - order_list: [] - }); - this.getOrderList(); - }, - - onClickLeft() { - wx.navigateBack() + loading: false, + // Caregiver mode + userType: '', + cgOrders: [], + cgPageNum: 1, + cgHasMore: true, + cgLoading: false, + // Promoter mode + comRecords: [], + comLoading: false, + // Worker mode + workRevenue: [], + workLoading: false, + workHasMore: true }, onLoad: function (options) { this.setData(getApp().getNavBarHeight()); }, + onShow: function () { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 2 }); + } + var userType = app.globalData.userType || 'C'; + var roleChanged = this.data.userType !== userType; + this.setData({ userType: userType }); + if (this.data.firstLoad || roleChanged) { + this.setData({ firstLoad: false }); + this.loadRoleContent(); + } + }, + + onPullDownRefresh: function () { + this.loadRoleContent().finally(function() { wx.stopPullDownRefresh(); }); + }, + + onReachBottom: function () { + if (this.data.userType === 'B') { + if (this.data.cgHasMore && !this.data.cgLoading) this.loadCgOrders(); + } else if ((this.data.userType === '03' || this.data.userType === '04' || this.data.userType === '05') && this.data.workHasMore && !this.data.workLoading) { + this.loadWorkRevenue(); + } else { + if (this.data.hasMore && !this.data.loading) this.getOrderList(); + } + }, + + loadRoleContent() { + var ut = this.data.userType; + if (ut === 'B') { + this.setData({ cgPageNum: 1, cgOrders: [] }); + this.loadCgOrders(); + } else if (ut === '06') { + this.loadCommission(); + } else if (ut === '03' || ut === '04' || ut === '05') { + this.loadWorkRevenue(); + } else { + this.setData({ pageNum: 1, order_list: [] }); + this.getOrderList(); + } + }, + + // === Promoter: Load commission records === + async loadCommission() { + this.setData({ comLoading: true }); + try { + var res = await request({ url: '/system/promotion/myCommission', method: 'GET', loading: false }); + if (res.code === 200) { + this.setData({ comRecords: res.data || [] }); + } + } catch (e) {} + this.setData({ comLoading: false }); + }, + + goWithdraw() { + wx.navigateTo({ url: '/shopping/pages/withdrawal/withdrawal' }); + }, + + // === Worker: Load revenue overview === + async loadWorkRevenue() { + this.setData({ workLoading: true }); + try { + var res = await request({ + url: '/system/profit/list', + method: 'GET', + data: { pageNum: 1, pageSize: 20 }, + loading: false + }); + if (res.code === 200) { + var list = res.rows || res.data || []; + this.setData({ workRevenue: list }); + } + } catch (e) {} + this.setData({ workLoading: false }); + }, + + goWorkOrderDetail(e) { + var item = JSON.stringify(e.currentTarget.dataset.item); + wx.navigateTo({ url: '/pages/orderdetail/orderdetail?item=' + item }); + }, + + // === Caregiver: Load completed orders (status=4,8) === + async loadCgOrders() { + if (this.data.cgLoading) return; + this.setData({ cgLoading: true }); + try { + var uid = wx.getStorageSync('uid'); + // Load completed orders: 已完成(4), 已结算(8) + var results = await Promise.all([ + request({ url: '/system/view/list', method: 'GET', data: { pageNum: 1, pageSize: 20, status: '4', userbId: uid }, loading: false }), + request({ url: '/system/view/list', method: 'GET', data: { pageNum: 1, pageSize: 20, status: '8', userbId: uid }, loading: false }) + ]); + var allList = []; + results.forEach(function(r) { + if (r.code === 200) { + var list = r.rows || r.data || []; + allList = allList.concat(list); + } + }); + allList.sort(function(a, b) { return (b.createTime || '').localeCompare(a.createTime || ''); }); + this.setData({ cgOrders: allList, cgHasMore: false, cgPageNum: 1 }); + } catch (e) {} + this.setData({ cgLoading: false }); + }, + + goCgDetail(e) { + var item = JSON.stringify(e.currentTarget.dataset.item); + wx.navigateTo({ url: '/pages/orderdetail/orderdetail?item=' + item }); + }, + + // === Patient: order list methods === + statusTap: function (e) { + var curType = e.currentTarget.dataset.index; + this.setData({ currentType: curType, pageNum: 1, order_list: [] }); + this.getOrderList(); + }, + + onClickLeft() { wx.navigateBack(); }, + itemclick(e) { if (e.currentTarget.dataset.item != 0) { - let item = JSON.stringify(e.currentTarget.dataset.item); - wx.navigateTo({ - url: "/pages/orderdetail/orderdetail?item=" + item, - }); + var item = JSON.stringify(e.currentTarget.dataset.item); + wx.navigateTo({ url: "/pages/orderdetail/orderdetail?item=" + item }); } }, go_pay(e) { if (e.currentTarget.dataset.item != 0) { - let item = JSON.stringify(e.currentTarget.dataset.item); - wx.navigateTo({ - url: "/pages/orderdetail/orderdetail?item=" + item, - }); - } - }, - - onShow: function () { - if (this.data.firstLoad) { - this.setData({ firstLoad: false }) - this.getOrderList() - } - }, - - onPullDownRefresh: function () { - this.setData({ pageNum: 1 }) - this.refreshList().finally(() => wx.stopPullDownRefresh()) - }, - - onReachBottom: function () { - if (this.data.hasMore && !this.data.loading) { - this.getOrderList() + var item = JSON.stringify(e.currentTarget.dataset.item); + wx.navigateTo({ url: "/pages/orderdetail/orderdetail?item=" + item }); } }, async refreshList() { - this.setData({ loading: true }) + this.setData({ loading: true }); try { var res = await request({ - url: '/system/view/list', - method: 'GET', - loading: false, - data: { - pageNum: 1, - pageSize: 30, - status: this.data.status[this.data.currentType], - usercId: wx.getStorageSync('uid') - } - }) + url: '/system/view/list', method: 'GET', loading: false, + data: { pageNum: 1, pageSize: 30, status: this.data.status[this.data.currentType], usercId: wx.getStorageSync('uid') } + }); var list = processOrderList(res.rows || res.data || []); - this.setData({ - order_list: list, - pageNum: 2, - hasMore: list.length >= 30 - }) - } catch (e) { - wx.showToast({ title: '加载失败', icon: 'none' }) - } - this.setData({ loading: false }) + this.setData({ order_list: list, pageNum: 2, hasMore: list.length >= 30 }); + } catch (e) { wx.showToast({ title: '加载失败', icon: 'none' }); } + this.setData({ loading: false }); }, async getOrderList() { - if (this.data.loading) return - this.setData({ loading: true }) + if (this.data.loading) return; + this.setData({ loading: true }); try { var res = await request({ - url: '/system/view/list', - method: 'GET', - loading: this.data.pageNum === 1, - data: { - pageNum: this.data.pageNum, - pageSize: 30, - status: this.data.status[this.data.currentType], - usercId: wx.getStorageSync('uid') - } - }) + url: '/system/view/list', method: 'GET', loading: this.data.pageNum === 1, + data: { pageNum: this.data.pageNum, pageSize: 30, status: this.data.status[this.data.currentType], usercId: wx.getStorageSync('uid') } + }); var newList = processOrderList(res.rows || res.data || []); if (this.data.pageNum === 1) { - this.setData({ - order_list: newList, - pageNum: 2, - hasMore: newList.length >= 30 - }) + this.setData({ order_list: newList, pageNum: 2, hasMore: newList.length >= 30 }); } else { - this.setData({ - order_list: this.data.order_list.concat(newList), - pageNum: this.data.pageNum + 1, - hasMore: newList.length >= 30 - }) + this.setData({ order_list: this.data.order_list.concat(newList), pageNum: this.data.pageNum + 1, hasMore: newList.length >= 30 }); } } catch (e) {} - this.setData({ loading: false }) - }, -}) + this.setData({ loading: false }); + } +}); diff --git a/coupon/pages/order/order.wxml b/coupon/pages/order/order.wxml index 2d6cdaf..41a96c0 100644 --- a/coupon/pages/order/order.wxml +++ b/coupon/pages/order/order.wxml @@ -1,51 +1,121 @@ - - - - - {{item}} - + + + + 推广佣金明细 + 提现 - - - - 暂无订单 - - - - - - - - - 订单号:{{item.orderNumber}} - - {{"已取消"}} - {{"拒绝接单"}} - {{"待接单"}} - {{"已接单"}} - {{"服务中"}} - {{"待支付"}} - {{"已完成"}} - {{"申请退款"}} - {{"退款中"}} - {{"已退款"}} - {{"已结算"}} + 加载中... + + + + + {{item.profitType === 'promotion_l1' ? '一级' : item.profitType === 'promotion_l2' ? '二级' : item.profitType === 'promotion_l3' ? '三级' : '推广'}} + 订单 #{{item.orderId}} + ¥{{item.amount || '0'}} + + + 比例 {{item.rate || '0'}}% + {{item.status === '1' ? '已结算' : item.status === '7' ? '已退回' : '待结算'}} + + {{item.createTime}} + + + 暂无佣金记录 + - - + + + 收益明细 + 加载中... + + + + 订单 #{{item.orderId}} + ¥{{item.amount || '0'}} + + + {{item.profitType === 'dispatch_fee' ? '调度费' : item.profitType === 'ops_flow' ? '运营流水' : item.profitType}} + {{item.status === '0' ? '未结算' : item.status === '1' ? '已结算' : item.status}} + + {{item.createTime}} + + + 暂无收益记录 + — 已加载全部 — + - - - {{item.userbName}} - {{item.serviceTime}}天 - 实付款:¥{{item.yuguMoney}}元 + + + + 已完成 + 已完成的陪诊/陪护订单 + + + + + {{item.yuliu1 || '订单#' + item.orderId}} + ¥{{item.yuguMoney || item.orderMoney}} + + + 患者ID: {{item.cId || '--'}} | {{item.hospitalName || ''}} + + + {{item.createTime}} + {{item.status === '4' ? '已完成' : item.status === '8' ? '已结算' : item.status}} + + + + 暂无已完成订单 + 加载中... + — 已加载全部 — + + + + + + + + + {{item}} + + + + + + 暂无订单 + + + + + + + + 订单号:{{item.orderNumber}} + {{"已取消"}} + {{"拒绝接单"}} + {{"待接单"}} + {{"已接单"}} + {{"服务中"}} + {{"待支付"}} + {{"已完成"}} + {{"申请退款"}} + {{"退款中"}} + {{"已退款"}} + {{"已结算"}} + + + + + + {{item.userbName}} + {{item.serviceTime}}天 + 实付款:¥{{item.yuguMoney}}元 + + 去付款 - 去付款 - - - \ No newline at end of file + diff --git a/coupon/pages/order/order.wxss b/coupon/pages/order/order.wxss index 921e9c4..b4495ea 100644 --- a/coupon/pages/order/order.wxss +++ b/coupon/pages/order/order.wxss @@ -190,5 +190,41 @@ padding: 0 31rpx; height: 128rpx; margin: 74rpx 40rpx 84rpx 40rpx; } +/* === Caregiver style === */ +.cg-container { min-height: 100vh; background: #F5F5F5; padding-bottom: 40rpx; } +.cg-header { background: #fff; padding: 32rpx 40rpx; margin-bottom: 16rpx; } +.cg-header-title { font-size: 36rpx; font-weight: bold; color: #333; display: block; } +.cg-header-sub { font-size: 24rpx; color: #999; margin-top: 8rpx; } +.cg-card { background: #fff; margin: 12rpx 0 0 0; padding: 24rpx 32rpx; } +.cg-row { display: flex; justify-content: space-between; align-items: center; } +.cg-row.sub { margin-top: 8rpx; } +.cg-order-no { font-size: 28rpx; font-weight: 600; color: #333; } +.cg-amount { font-size: 30rpx; font-weight: 700; color: #E6A23C; } +.cg-meta { font-size: 24rpx; color: #999; } +.cg-time { font-size: 22rpx; color: #C0C4CC; } +.cg-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; } +.cg-status.s-done { background: #E8F5E9; color: #4CAF50; } +.cg-status.s-settled { background: #E3F2FD; color: #2196F3; } +.loading { text-align: center; padding: 40rpx; color: #999; } +.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 28rpx; } +.end { text-align: center; padding: 32rpx; color: #C0C4CC; font-size: 24rpx; } - \ No newline at end of file +/* === Multi-role shared styles === */ +.container { padding: 0 0 40rpx 0; background: #F5F5F5; min-height: 100vh; } +.summary-bar { display: flex; justify-content: space-between; align-items: center; padding: 24rpx 32rpx; background: #fff; margin-bottom: 12rpx; } +.summary-label { font-size: 30rpx; font-weight: bold; color: #333; } +.withdraw-btn { background: #FF8F1F; color: #fff; font-size: 24rpx; padding: 8rpx 24rpx; border-radius: 24rpx; } + +.record-card { background: #fff; margin: 12rpx 0 0 0; padding: 24rpx 32rpx; } +.record-row { display: flex; justify-content: space-between; align-items: center; } +.record-row.sub { margin-top: 8rpx; } +.record-type { display: flex; align-items: center; } +.type-tag { font-size: 22rpx; background: #FFF3E0; color: #FF9800; padding: 2rpx 12rpx; border-radius: 4rpx; margin-right: 12rpx; } +.order-id { font-size: 24rpx; color: #999; } +.record-amount { font-size: 30rpx; font-weight: 700; color: #67C23A; } +.record-rate { font-size: 22rpx; color: #999; } +.record-status { font-size: 22rpx; padding: 2rpx 12rpx; border-radius: 4rpx; } +.record-status.settled { background: #E8F5E9; color: #4CAF50; } +.record-status.reversed { background: #FFEBEE; color: #F44336; } +.record-status.pending { background: #FFF3E0; color: #FF9800; } +.record-time { font-size: 22rpx; color: #C0C4CC; margin-top: 4rpx; } diff --git a/coupon/pages/peihu/peihu.js b/coupon/pages/peihu/peihu.js index e9997b5..37ef818 100644 --- a/coupon/pages/peihu/peihu.js +++ b/coupon/pages/peihu/peihu.js @@ -60,7 +60,7 @@ Page({ }).then(res => { var list = res.data || []; list = list.map(function(item) { - item.facePic = app.fixImageUrl(item.facePic); + item.facePic = app.fixImageUrl(item.facePic || item.avatar) || '/pages/image/userimg.png'; return item; }); that.setData({ special_offer: list }) diff --git a/coupon/pages/peihu/peihu.wxml b/coupon/pages/peihu/peihu.wxml index 2c73400..4a08831 100644 --- a/coupon/pages/peihu/peihu.wxml +++ b/coupon/pages/peihu/peihu.wxml @@ -21,18 +21,18 @@ - + - + {{item.nickName}} ({{"陪护"}}) - {{"男"}} - {{"女"}} + 男 + 女 {{item.experience}}经验 diff --git a/coupon/pages/peizhen/peizhen.js b/coupon/pages/peizhen/peizhen.js index a778676..05a11b0 100644 --- a/coupon/pages/peizhen/peizhen.js +++ b/coupon/pages/peizhen/peizhen.js @@ -59,7 +59,7 @@ Page({ if (services.length) { var mapped = services.map(function (s) { return { - icon: s.icon || s.name, + icon: s.icon || '', title: s.name, price: s.price, describe: s.description || '', diff --git a/coupon/pages/peizhen/peizhen.wxml b/coupon/pages/peizhen/peizhen.wxml index e068fe9..bc5e9eb 100644 --- a/coupon/pages/peizhen/peizhen.wxml +++ b/coupon/pages/peizhen/peizhen.wxml @@ -21,10 +21,10 @@ - + - + diff --git a/coupon/pages/peizhen/peizhendetail.wxml b/coupon/pages/peizhen/peizhendetail.wxml index 1a31902..d3ce0ee 100644 --- a/coupon/pages/peizhen/peizhendetail.wxml +++ b/coupon/pages/peizhen/peizhendetail.wxml @@ -6,7 +6,7 @@ - + diff --git a/coupon/pages/reforderdetail/reforderdetail.js b/coupon/pages/reforderdetail/reforderdetail.js index 41c37ba..a591ca9 100644 --- a/coupon/pages/reforderdetail/reforderdetail.js +++ b/coupon/pages/reforderdetail/reforderdetail.js @@ -6,6 +6,7 @@ Page({ * 页面的初始数据 */ data: { + height: '', resp:{ "erro": 0, "infro": { @@ -83,6 +84,7 @@ Page({ * 生命周期函数--监听页面加载 */ onLoad: function (options) { + this.setData(getApp().getNavBarHeight()); console.log(options.type) console.log(options.id) //详情显示 diff --git a/coupon/pages/reforderdetail/reforderdetail.json b/coupon/pages/reforderdetail/reforderdetail.json index 7249a3c..bfe1c17 100644 --- a/coupon/pages/reforderdetail/reforderdetail.json +++ b/coupon/pages/reforderdetail/reforderdetail.json @@ -3,5 +3,6 @@ "van-nav-bar":"/miniprogram/miniprogram_npm/@vant/weapp/nav-bar/index", "van-uploader": "/miniprogram/miniprogram_npm/@vant/weapp/uploader/index" }, - "navigationBarTitleText": "订单详情" + "navigationBarTitleText": "退款详情", + "navigationStyle": "custom" } \ No newline at end of file diff --git a/coupon/pages/reforderdetail/reforderdetail.wxml b/coupon/pages/reforderdetail/reforderdetail.wxml index bbe0560..3f3f4a5 100644 --- a/coupon/pages/reforderdetail/reforderdetail.wxml +++ b/coupon/pages/reforderdetail/reforderdetail.wxml @@ -1,4 +1,12 @@ + + @@ -137,6 +145,7 @@ + diff --git a/coupon/pages/service/service.js b/coupon/pages/service/service.js index 3225183..4a87e56 100644 --- a/coupon/pages/service/service.js +++ b/coupon/pages/service/service.js @@ -1,87 +1,239 @@ -// 服务模块 var app = getApp(); const { request } = require('../../utils/request') Page({ data: { + // Patient mode servicesPeizhen: [], servicesPeihu: [], activeTab: 0, - firstLoad: true + firstLoad: true, + // Caregiver mode + userType: '', + cgOrders: [], + cgPageNum: 1, + cgHasMore: true, + cgLoading: false, + // Promoter mode + teamMembers: [], + teamLoading: false, + // Worker mode + workOrders: [], + workLoading: false, + workPageNum: 1, + workHasMore: true }, onShow() { - if (this.data.firstLoad) { - this.setData({ firstLoad: false }) - this.loadServices() + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 1 }); + } + var userType = app.globalData.userType || 'C'; + var roleChanged = this.data.userType !== userType; + this.setData({ userType: userType }); + if (this.data.firstLoad || roleChanged) { + this.setData({ firstLoad: false }); + this.loadRoleContent(); } }, onPullDownRefresh() { - this.loadServices().finally(() => wx.stopPullDownRefresh()) + this.loadRoleContent().finally(function() { wx.stopPullDownRefresh(); }); }, + onReachBottom() { + if (this.data.userType === 'B' && this.data.cgHasMore && !this.data.cgLoading) { + this.loadCgOrders(); + } else if ((this.data.userType === '03' || this.data.userType === '04' || this.data.userType === '05') && this.data.workHasMore && !this.data.workLoading) { + this.loadWorkOrders(); + } + }, + + loadRoleContent() { + var ut = this.data.userType; + if (ut === 'B') { + this.setData({ cgPageNum: 1, cgOrders: [] }); + this.loadCgOrders(); + } else if (ut === '06') { + this.loadTeam(); + } else if (ut === '03' || ut === '04' || ut === '05') { + this.setData({ workPageNum: 1, workOrders: [] }); + this.loadWorkOrders(); + } else { + this.loadServices(); + } + }, + + // === Promoter: Load team === + async loadTeam() { + this.setData({ teamLoading: true }); + try { + var res = await request({ url: '/system/promotion/team', method: 'GET', loading: false }); + if (res.code === 200) { + this.setData({ teamMembers: res.data || [] }); + } + } catch (e) {} + this.setData({ teamLoading: false }); + }, + + // === Worker (03/04/05): Load all orders === + async loadWorkOrders() { + if (this.data.workLoading) return; + this.setData({ workLoading: true }); + try { + var res = await request({ + url: '/system/view/list', + method: 'GET', + data: { pageNum: this.data.workPageNum, pageSize: 20 }, + loading: false + }); + if (res.code === 200) { + var list = res.rows || res.data || []; + if (this.data.workPageNum === 1) { + this.setData({ workOrders: list, workHasMore: list.length >= 20, workPageNum: 2 }); + } else { + this.setData({ + workOrders: this.data.workOrders.concat(list), + workHasMore: list.length >= 20, + workPageNum: this.data.workPageNum + 1 + }); + } + } + } catch (e) {} + this.setData({ workLoading: false }); + }, + + goWorkDetail(e) { + var item = JSON.stringify(e.currentTarget.dataset.item); + wx.navigateTo({ url: '/pages/orderdetail/orderdetail?item=' + item }); + }, + + // === Caregiver: Load in-progress orders (status=1,2,3) === + async loadCgOrders() { + if (this.data.cgLoading) return; + this.setData({ cgLoading: true }); + try { + var uid = wx.getStorageSync('uid'); + // Load in-progress orders: 已接单(1), 服务中(2), 待支付(3) + var results = await Promise.all([ + request({ url: '/system/view/list', method: 'GET', data: { pageNum: 1, pageSize: 20, status: '1', userbId: uid }, loading: false }), + request({ url: '/system/view/list', method: 'GET', data: { pageNum: 1, pageSize: 20, status: '2', userbId: uid }, loading: false }), + request({ url: '/system/view/list', method: 'GET', data: { pageNum: 1, pageSize: 20, status: '3', userbId: uid }, loading: false }) + ]); + var allList = []; + results.forEach(function(r) { + if (r.code === 200) { + var list = r.rows || r.data || []; + allList = allList.concat(list); + } + }); + // sort by createTime desc + allList.sort(function(a, b) { return (b.createTime || '').localeCompare(a.createTime || ''); }); + this.setData({ cgOrders: allList, cgHasMore: false, cgPageNum: 1 }); + } catch (e) {} + this.setData({ cgLoading: false }); + }, + + // === Caregiver: Navigate to order detail === + goCgDetail(e) { + var item = JSON.stringify(e.currentTarget.dataset.item); + wx.navigateTo({ url: '/pages/orderdetail/orderdetail?item=' + item }); + }, + + // === Caregiver: Start service (status 1→2) === + startService(e) { + var item = e.currentTarget.dataset.item; + var that = this; + wx.showModal({ + title: '确认开始服务', + content: '确认到达医院并开始为 ' + (item.yuliu1 || '订单#' + item.orderId) + ' 服务?', + success: function(res) { if (res.confirm) that.doStartService(item); } + }); + }, + + async doStartService(item) { + try { + var res = await request({ + url: '/system/view/startServiceWithOrder', + method: 'POST', + data: { orderId: item.orderId }, + loading: true, + loadingText: '处理中...' + }); + if (res.code === 200) { + wx.showToast({ title: '已开始服务', icon: 'success' }); + this.setData({ firstLoad: true }); + this.onShow(); + } + } catch (e) {} + }, + + // === Caregiver: Complete service (status 2→3) === + completeService(e) { + var item = e.currentTarget.dataset.item; + var that = this; + wx.showModal({ + title: '确认完成服务', + content: '确认完成 ' + (item.yuliu1 || '订单#' + item.orderId) + ' 的服务?完成后将等待患者付款。', + success: function(res) { if (res.confirm) that.doCompleteService(item); } + }); + }, + + async doCompleteService(item) { + try { + var res = await request({ + url: '/system/view/completeOrder', + method: 'POST', + data: { orderId: item.orderId }, + loading: true, + loadingText: '处理中...' + }); + if (res.code === 200) { + wx.showToast({ title: '服务已完成', icon: 'success' }); + this.setData({ firstLoad: true }); + this.onShow(); + } + } catch (e) {} + }, + + getCgStatusLabel: function(s) { + return getApp().getStatusLabel(s); + }, + + // === Patient: Load services === async loadServices() { try { - // 同时加载陪诊和陪护服务 - var [res1, res2] = await Promise.all([ - request({ url: '/system/serviceType/enabled', data: { category: '2' }, loading: true, loadingText: '加载服务...' }), - request({ url: '/system/serviceType/enabled', data: { category: '1' }, loading: false }) - ]) + var res1 = await request({ url: '/system/serviceType/enabled', data: { category: '2' }, loading: true, loadingText: '加载服务...' }); + var res2 = await request({ url: '/system/serviceType/enabled', data: { category: '1' }, loading: false }); this.setData({ servicesPeizhen: (res1.data || []).map(function(s) { - return { - id: s.id, - name: s.name, - title: s.name, - price: s.price, - desc: s.description, - describe: s.description, - icon: s.icon, - ordertype: s.typeCode, - typeCode: s.typeCode - } + return { id: s.id, name: s.name, title: s.name, price: s.price, desc: s.description, describe: s.description, icon: s.icon, ordertype: s.typeCode, typeCode: s.typeCode }; }), servicesPeihu: (res2.data || []).map(function(s) { - return { - id: s.id, - name: s.name, - title: s.name, - price: s.price, - desc: s.description, - describe: s.description, - icon: s.icon, - ordertype: s.typeCode, - typeCode: s.typeCode, - nickName: s.name, - introduce: s.description - } + return { id: s.id, name: s.name, title: s.name, price: s.price, desc: s.description, describe: s.description, icon: s.icon, ordertype: s.typeCode, typeCode: s.typeCode, nickName: s.name, introduce: s.description }; }) - }) + }); } catch (e) {} }, switchTab(e) { - this.setData({ activeTab: e.currentTarget.dataset.index }) + this.setData({ activeTab: e.currentTarget.dataset.index }); }, - // 点击服务-跳转下单页 onServiceTap(e) { if (wx.getStorageSync('phone') == '') { wx.showModal({ title: '提示', content: '请先登录', - success(res) { - if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) - } - }) - return + success: function(res) { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }); } + }); + return; } - var item = JSON.stringify(e.currentTarget.dataset.item) + var item = JSON.stringify(e.currentTarget.dataset.item); if (this.data.activeTab === 0) { - wx.navigateTo({ url: '/pages/peizhen/peizhendetail?item=' + item }) + wx.navigateTo({ url: '/pages/peizhen/peizhendetail?item=' + item }); } else { - wx.navigateTo({ url: '/shopping/pages/goddess/goddess?item=' + item }) + wx.navigateTo({ url: '/shopping/pages/goddess/goddess?item=' + item }); } } -}) +}); diff --git a/coupon/pages/service/service.wxml b/coupon/pages/service/service.wxml index d1fe9aa..7319d1f 100644 --- a/coupon/pages/service/service.wxml +++ b/coupon/pages/service/service.wxml @@ -1,11 +1,83 @@ - - + + + 我的团队共{{teamMembers.length}}人 + 加载中... + + + + {{item.nickName || '用户' + item.userId}} + {{item.phonenumber}} + + + {{item.inviteCount || 0}}邀请 + ¥{{item.commission || '0'}}佣金贡献 + L{{item.level}} + + + + 暂无团队成员 + + + + + 全部订单 + + + + {{item.yuliu1 || '订单#' + item.orderId}} + ¥{{item.yuguMoney || item.orderMoney}} + + + 患者ID: {{item.cId || '--'}} | 陪诊师ID: {{item.bId || '--'}} + + + {{item.createTime}} + {{item.status === '0' ? '待接单' : item.status === '1' ? '已接单' : item.status === '2' ? '服务中' : item.status === '4' ? '已完成' : item.status === '5' ? '退款中' : item.status === '7' ? '已退款' : item.status === '8' ? '已结算' : item.status}} + + + + 暂无订单 + 加载中... + — 已加载全部 — + + + + + + 进行中 + 正在服务的订单 + + + + + {{item.yuliu1 || '订单#' + item.orderId}} + ¥{{item.yuguMoney || item.orderMoney}} + + + 患者ID: {{item.cId || '--'}} | {{item.hospitalName || ''}} + + + {{item.createTime}} + {{item.status === '1' ? '已接单' : item.status === '2' ? '服务中' : item.status === '3' ? '待支付' : item.status}} + + + 开始服务 + 完成服务 + + + + 暂无进行中订单 + 加载中... + — 已加载全部 — + + + + 全部服务 专业陪诊陪护,全程无忧 - 就医陪诊 @@ -17,13 +89,12 @@ - - + @@ -42,13 +113,12 @@ - - + diff --git a/coupon/pages/service/service.wxss b/coupon/pages/service/service.wxss index 0eb2c59..a4276db 100644 --- a/coupon/pages/service/service.wxss +++ b/coupon/pages/service/service.wxss @@ -132,3 +132,47 @@ color: #999; font-size: 28rpx; } + +/* === Caregiver style === */ +.cg-container { min-height: 100vh; background: #F5F5F5; padding-bottom: 40rpx; } +.cg-header { background: #fff; padding: 32rpx 40rpx; margin-bottom: 16rpx; } +.cg-header-title { font-size: 36rpx; font-weight: bold; color: #333; display: block; } +.cg-header-sub { font-size: 24rpx; color: #999; margin-top: 8rpx; } +.cg-card { background: #fff; margin: 12rpx 0 0 0; padding: 24rpx 32rpx; } +.cg-row { display: flex; justify-content: space-between; align-items: center; } +.cg-row.sub { margin-top: 8rpx; } +.cg-order-no { font-size: 28rpx; font-weight: 600; color: #333; } +.cg-amount { font-size: 30rpx; font-weight: 700; color: #E6A23C; } +.cg-meta { font-size: 24rpx; color: #999; } +.cg-time { font-size: 22rpx; color: #C0C4CC; } +.cg-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; } +.cg-status.s-accepted { background: #FFF3E0; color: #FF9800; } +.cg-status.s-serving { background: #E3F2FD; color: #2196F3; } +.cg-status.s-pay { background: #FCE4EC; color: #E91E63; } +.loading { text-align: center; padding: 40rpx; color: #999; } +.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 28rpx; } +.end { text-align: center; padding: 32rpx; color: #C0C4CC; font-size: 24rpx; } + +/* === Multi-role shared styles === */ +.container { padding: 0 0 40rpx 0; background: #F5F5F5; min-height: 100vh; } +.header { background: #fff; padding: 32rpx 40rpx; margin-bottom: 16rpx; display: flex; justify-content: space-between; align-items: center; } +.title { font-size: 36rpx; font-weight: bold; color: #333; } +.count { font-size: 24rpx; color: #999; } + +.member-card { background: #fff; margin: 12rpx 0 0 0; padding: 24rpx 32rpx; } +.member-info { display: flex; align-items: center; } +.member-name { font-size: 28rpx; font-weight: 600; color: #333; } +.member-phone { font-size: 24rpx; color: #999; margin-left: 16rpx; } +.member-stats { display: flex; margin-top: 12rpx; } +.member-stat { text-align: center; margin-right: 32rpx; } +.ms-num { font-size: 28rpx; font-weight: 700; color: #333; display: block; } +.ms-num.gold { color: #FF8F1F; } +.ms-label { font-size: 20rpx; color: #999; } + +.cg-row.actions { margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid #F0F0F0; justify-content: flex-end; } +.cg-start-btn { background: #67C23A; color: #fff; font-size: 24rpx; padding: 8rpx 28rpx; border-radius: 24rpx; } +.cg-done-btn { background: #FF8F1F; color: #fff; font-size: 24rpx; padding: 8rpx 28rpx; border-radius: 24rpx; } + +.cg-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; background: #409EFF; color: #fff; } +.cg-status.s-done { background: #E8F5E9; color: #4CAF50; } +.cg-status.s-settled { background: #E0F2F1; color: #009688; } diff --git a/coupon/pages/wodeqianbao/qianbao.js b/coupon/pages/wodeqianbao/qianbao.js index 66b476a..17292ed 100644 --- a/coupon/pages/wodeqianbao/qianbao.js +++ b/coupon/pages/wodeqianbao/qianbao.js @@ -42,24 +42,28 @@ Page({ getGoodsList: function () { var that = this; return request({ - url: '/app/Goddesswechat/getCapitalDetails', - method: 'POST', + url: '/system/detail/list', + method: 'GET', loading: true, data: { - uid: wx.getStorageSync('uid'), - token: wx.getStorageSync('token'), - page: String(this.data.page), - page_size: 10, + pageNum: this.data.page, + pageSize: 10, } }).then(res => { - if (res.code === 200 || res.erro === 0) { - var list = res.detail || res.data || []; + if (res.code === 200) { + var list = (res.rows || []).map(function(item) { + return { + id: item.id, + type: item.type === '1' ? 1 : -1, + time: item.createTime || '', + money: item.jine || '0.00' + }; + }); if (that.data.page === 1) { that.setData({ order_list: list }) } else { that.setData({ order_list: that.data.order_list.concat(list) }) } - that.setData({ page: that.data.page + 1 }) } }).catch(function() { wx.showToast({ title: '加载失败', icon: 'none' }) diff --git a/coupon/pages/wodeqianbao/qianbao.json b/coupon/pages/wodeqianbao/qianbao.json index 1d20fd7..487b35a 100644 --- a/coupon/pages/wodeqianbao/qianbao.json +++ b/coupon/pages/wodeqianbao/qianbao.json @@ -2,6 +2,5 @@ "usingComponents": { "van-nav-bar":"/miniprogram/miniprogram_npm/@vant/weapp/nav-bar/index" }, - "navigationBarTitleText": "我的钱包", - "navigationStyle": "custom" + "navigationBarTitleText": "我的钱包" } diff --git a/coupon/pages/wodeqianbao/qianbao.wxml b/coupon/pages/wodeqianbao/qianbao.wxml index ae607ed..05f1dcb 100644 --- a/coupon/pages/wodeqianbao/qianbao.wxml +++ b/coupon/pages/wodeqianbao/qianbao.wxml @@ -8,7 +8,7 @@ - + 退款到账 @@ -25,4 +25,9 @@ 暂无交易记录 + + + + + diff --git a/coupon/pages/wodeqianbao/qianbao.wxss b/coupon/pages/wodeqianbao/qianbao.wxss index 3b6b16b..7367eda 100644 --- a/coupon/pages/wodeqianbao/qianbao.wxss +++ b/coupon/pages/wodeqianbao/qianbao.wxss @@ -1,116 +1,73 @@ /* pages/activity/activity.wxss */ .Group { - height: 120rpx; - display: flex; - border-bottom: 1px solid #f5f5f5; - align-items: center; - } - - - - .Group-image image { - width: 100%; - height: 100%; - } - - - - .title1 { - font-size: 28rpx; - color: #3D3D3D; - } - - .title2 { - font-size: 26rpx; - color: #A8A8A8; - margin-top: 8rpx; - } - - .content .title2 .house { - width: 28rpx; - height: 28rpx; - margin-right: 7rpx; - margin-top: 5rpx; - } - - .content .title2 .house image { - width: 100%; - height: 100%; - } - - - - .Price1 { - display: flex; - font-size: 28rpx; - color: #FF4A4A; - line-height: 42rpx; - margin-right: 17rpx; - } - - .content .Price .Price2 { - font-size: 24rpx; - color: #CBCBCB; - line-height: 41rpx; - margin-right: 23rpx; - text-decoration:line-through; - } - - .content .Price .Price3 { - width: 51rpx; - height: 21rpx; - font-size: 15rpx; - color: #E75D4D; - border: 1px solid #E75D4D; - border-radius: 4rpx; - text-align: center; - line-height: 18rpx; - margin-top: 10rpx; - } - - .order-list{ - width: 100%; - } + height: 120rpx; + display: flex; + border-bottom: 1px solid #f5f5f5; + align-items: center; +} +.Group-image image { + width: 100%; + height: 100%; +} - /* _______________________________________ */ - view { - box-sizing: border-box; - overflow: hidden; - } - .custom-bar { - /* background-color: #aaa; */ - position: fixed; - left: 0; - top: 0; - width: 100%; - } - .custom-bar__wrapper { - padding: 0 10rpx; - display: flex; - justify-content: center; - align-items: center; - } - .search-group { - width: 100%; - height: 100%; - display: flex; - justify-content: flex-start; - align-items: center; - border-radius: 60rpx; - /* padding: 0 10rpx; */ - padding-left: 25rpx; - background: #F5F5F5; - } - .search-group > input { - font-size: 25rpx; - width: 800rpx; - } - .search-group > image { - height: 32rpx; - width: 32rpx; - margin-right: 20rpx - } - \ No newline at end of file + + +.title1 { + font-size: 28rpx; + color: #3D3D3D; +} + +.title2 { + font-size: 26rpx; + color: #A8A8A8; + margin-top: 8rpx; +} + +.content .title2 .house { + width: 28rpx; + height: 28rpx; + margin-right: 7rpx; + margin-top: 5rpx; +} + +.content .title2 .house image { + width: 100%; + height: 100%; +} + + + +.Price1 { + display: flex; + font-size: 28rpx; + color: #FF4A4A; + line-height: 42rpx; + margin-right: 17rpx; +} + +.content .Price .Price2 { + font-size: 24rpx; + color: #CBCBCB; + line-height: 41rpx; + margin-right: 23rpx; + text-decoration:line-through; +} + +.content .Price .Price3 { + width: 51rpx; + height: 21rpx; + font-size: 15rpx; + color: #E75D4D; + border: 1px solid #E75D4D; + border-radius: 4rpx; + text-align: center; + line-height: 18rpx; + margin-top: 10rpx; +} + +.order-list{ + width: 100%; +} diff --git a/coupon/shopping/pages/goddess/goddess.js b/coupon/shopping/pages/goddess/goddess.js index ec08e42..73c074b 100644 --- a/coupon/shopping/pages/goddess/goddess.js +++ b/coupon/shopping/pages/goddess/goddess.js @@ -30,10 +30,19 @@ Page({ submitting: false, clock: '', address: '', - danjia: 0.01, + danjia: 400, beizhu: '', + contactName: '', + contactPhone: '', + ward: '', + hospitalName: '', + hospitalAddress: '', }, + onContactName(e) { this.setData({ contactName: e.detail.value }) }, + onContactPhone(e) { this.setData({ contactPhone: e.detail.value }) }, + onWard(e) { this.setData({ ward: e.detail.value }) }, + // 确认支付 confirm_pay() { if (this.data.submitting) return @@ -57,6 +66,12 @@ Page({ return } + // 校验必填信息 + if (!this.data.contactPhone) { + wx.showToast({ title: '请填写联系电话', icon: 'none' }) + return + } + this.setData({ submitting: true }) try { var res = await request({ @@ -74,6 +89,10 @@ Page({ hospitalId: this.data.item.hospitalId, cId: wx.getStorageSync('uid'), remark: this.data.beizhu, + yuliu1: this.data.contactName, + yuliu2: this.data.contactPhone, + yuliu3: this.data.ward, + yuliu4: this.data.hospitalName, }, header: { 'content-type': 'application/json' } }) @@ -118,17 +137,42 @@ Page({ this.setData(getApp().getNavBarHeight()); var that = this; let item = JSON.parse(e.item); + item.facePic = app.fixImageUrl(item.facePic || item.avatar) || '/pages/image/userimg.png'; that.setData({ - item: item + item: item, + charge_phone: item.phonenumber || '', + contactName: wx.getStorageSync('nickName') || '', + contactPhone: wx.getStorageSync('phone') || '' }); + + // 加载医院信息 + if (item.hospitalId) { + request({ + url: '/system/hospital/list', + method: 'GET', + loading: false + }).then(function(res) { + var hospitals = res.rows || res.data || []; + var hospital = hospitals.find(function(h) { return h.id == item.hospitalId }); + if (hospital) { + that.setData({ + hospitalName: hospital.hospitalname || '', + hospitalAddress: hospital.address || '' + }); + } + }); + } }, /** * 拨打电话 */ - callphone() { - wx.makePhoneCall({ - phoneNumber: this.data.charge_phone, - }) + callphone(e) { + var phone = e.currentTarget.dataset.phone || this.data.item.phonenumber || ''; + if (!phone) { + wx.showToast({ title: '暂无电话号码', icon: 'none' }); + return; + } + wx.makePhoneCall({ phoneNumber: phone }) }, /** diff --git a/coupon/shopping/pages/goddess/goddess.wxml b/coupon/shopping/pages/goddess/goddess.wxml index 8bec3fa..6fec8e4 100644 --- a/coupon/shopping/pages/goddess/goddess.wxml +++ b/coupon/shopping/pages/goddess/goddess.wxml @@ -6,18 +6,18 @@ - + {{item.nickName}} ({{"陪护员"}}) - {{"男"}} - {{"女"}} + 男 + 女 {{item.experience}}经验 - {{"简介:从事陪诊服务2年,服务细心,热情周到,有耐心、责任心强。"}} + 简介:{{item.introduce || '暂无简介'}} @@ -27,12 +27,58 @@ 电话:{{item.phonenumber}} - + 地址:{{item.address}} + + + {{"客户信息"}} + + + + + 联系人 + + + + + + 联系电话 + + + + + + + + + {{"陪护地点"}} + + + + + 医院名称 + + {{hospitalName || '未指定'}} + + + + 医院地址 + + {{hospitalAddress || '未指定'}} + + + + 科室/病房 + + + + + + {{"陪护时间选择"}} @@ -75,7 +121,7 @@ - 计费说明:一小时50元,服务时间(上午8:30-12:30下午14:00-18:00) + 计费说明:400元/天(50元/小时 × 8小时),服务时间 上午8:30-12:30 下午14:00-18:00 diff --git a/coupon/shopping/pages/withdrawal/withdrawal.js b/coupon/shopping/pages/withdrawal/withdrawal.js index 9ca0c74..df4ce48 100644 --- a/coupon/shopping/pages/withdrawal/withdrawal.js +++ b/coupon/shopping/pages/withdrawal/withdrawal.js @@ -7,6 +7,7 @@ Page({ * 页面的初始数据 */ data: { + height: '', pingtainame:'微信', pingtaiimage:'/shopping/image/WeChat.png', second:'', @@ -21,6 +22,9 @@ Page({ { name: '支付宝',color: '#464646;',imagesrc:'/shopping/image/Alipay.png'}, ], }, + back() { + wx.navigateBack() + }, onClose() { // wx.showToast({ title: '点击成功', icon: 'none' }); this.setData({ show: false }); @@ -78,11 +82,11 @@ Page({ * 生命周期函数--监听页面加载 */ onLoad: function (options) { + this.setData(getApp().getNavBarHeight()); console.log(options.money) this.setData({ money:options.money }) - }, /** diff --git a/coupon/shopping/pages/withdrawal/withdrawal.json b/coupon/shopping/pages/withdrawal/withdrawal.json index edba86c..d307ae7 100644 --- a/coupon/shopping/pages/withdrawal/withdrawal.json +++ b/coupon/shopping/pages/withdrawal/withdrawal.json @@ -1,7 +1,8 @@ { "usingComponents": { + "van-nav-bar":"/miniprogram/miniprogram_npm/@vant/weapp/nav-bar/index", "van-action-sheet": "/miniprogram/miniprogram_npm/@vant/weapp/action-sheet/index" }, - "navigationBarTitleText": "提现" - + "navigationBarTitleText": "提现", + "navigationStyle": "custom" } \ No newline at end of file diff --git a/coupon/shopping/pages/withdrawal/withdrawal.wxml b/coupon/shopping/pages/withdrawal/withdrawal.wxml index 61e23cd..1b56c98 100644 --- a/coupon/shopping/pages/withdrawal/withdrawal.wxml +++ b/coupon/shopping/pages/withdrawal/withdrawal.wxml @@ -1,5 +1,13 @@ + + 提现至: @@ -39,3 +47,4 @@ bind:cancel ="onCancel" bind:select="onSelect" /> + diff --git a/coupon/subPackages/pages-promoter/commission/commission.js b/coupon/subPackages/pages-promoter/commission/commission.js new file mode 100644 index 0000000..e3db2a1 --- /dev/null +++ b/coupon/subPackages/pages-promoter/commission/commission.js @@ -0,0 +1,36 @@ +var { request } = require('../../../utils/request'); + +Page({ + data: { + records: [], + loading: true + }, + + onShow() { this.loadRecords(); }, + + onPullDownRefresh() { this.loadRecords().finally(function() { wx.stopPullDownRefresh(); }); }, + + async loadRecords() { + this.setData({ loading: true }); + try { + var res = await request({ url: '/system/promotion/myCommission', method: 'GET', loading: false }); + if (res.code === 200) { + this.setData({ records: res.data || [], loading: false }); + } + } catch (e) { this.setData({ loading: false }); } + }, + + goWithdraw() { + wx.navigateTo({ url: '/shopping/pages/withdrawal/withdrawal' }); + }, + + getTypeLabel(t) { + var map = { 'promotion_l1': '一级推广', 'promotion_l2': '二级推广', 'promotion_l3': '三级推广' }; + return map[t] || t || '推广佣金'; + }, + + getStatusLabel(s) { + var map = { '0': '待结算', '1': '已结算', '7': '已退回' }; + return map[s] || s || '待结算'; + } +}); diff --git a/coupon/subPackages/pages-promoter/commission/commission.json b/coupon/subPackages/pages-promoter/commission/commission.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/coupon/subPackages/pages-promoter/commission/commission.json @@ -0,0 +1 @@ +{} diff --git a/coupon/subPackages/pages-promoter/commission/commission.wxml b/coupon/subPackages/pages-promoter/commission/commission.wxml new file mode 100644 index 0000000..eaa084d --- /dev/null +++ b/coupon/subPackages/pages-promoter/commission/commission.wxml @@ -0,0 +1,27 @@ + + + 推广佣金明细 + 提现 + + + 加载中... + + + + + + {{item.profitType === 'promotion_l1' ? '一级' : item.profitType === 'promotion_l2' ? '二级' : item.profitType === 'promotion_l3' ? '三级' : '推广'}} + 订单 #{{item.orderId}} + + ¥{{item.amount || '0'}} + + + 比例 {{item.rate || '0'}}% + {{item.status === '1' ? '已结算' : item.status === '7' ? '已退回' : '待结算'}} + + {{item.createTime}} + + + + 暂无佣金记录 + diff --git a/coupon/subPackages/pages-promoter/commission/commission.wxss b/coupon/subPackages/pages-promoter/commission/commission.wxss new file mode 100644 index 0000000..0546bf5 --- /dev/null +++ b/coupon/subPackages/pages-promoter/commission/commission.wxss @@ -0,0 +1,20 @@ +.container { padding: 0 0 40rpx 0; background: #F5F5F5; min-height: 100vh; } +.summary-bar { display: flex; justify-content: space-between; align-items: center; padding: 24rpx 32rpx; background: #fff; margin-bottom: 4rpx; } +.summary-label { font-size: 32rpx; font-weight: 700; color: #333; } +.withdraw-btn { font-size: 26rpx; color: #fff; background: #FF8F1F; padding: 10rpx 24rpx; border-radius: 32rpx; } + +.loading { text-align: center; padding: 80rpx; color: #999; } +.record-card { background: #fff; margin: 12rpx 0 0 0; padding: 28rpx 32rpx; } +.record-row { display: flex; justify-content: space-between; align-items: center; } +.record-row.sub { margin-top: 12rpx; } +.record-type { display: flex; align-items: center; } +.type-tag { font-size: 22rpx; background: #FFF3E0; color: #FF9800; padding: 4rpx 12rpx; border-radius: 8rpx; } +.order-id { font-size: 24rpx; color: #999; margin-left: 12rpx; } +.record-amount { font-size: 34rpx; font-weight: 700; color: #333; } +.record-rate { font-size: 24rpx; color: #999; } +.record-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; } +.record-status.settled { background: #E8F5E9; color: #4CAF50; } +.record-status.pending { background: #FFF3E0; color: #FF9800; } +.record-status.reversed { background: #FFEBEE; color: #F44336; } +.record-time { font-size: 22rpx; color: #C0C4CC; margin-top: 12rpx; } +.empty { text-align: center; padding: 120rpx; color: #999; font-size: 28rpx; } diff --git a/coupon/subPackages/pages-promoter/dashboard/dashboard.js b/coupon/subPackages/pages-promoter/dashboard/dashboard.js new file mode 100644 index 0000000..9ebd5f8 --- /dev/null +++ b/coupon/subPackages/pages-promoter/dashboard/dashboard.js @@ -0,0 +1,27 @@ +var app = getApp(); +var { request } = require('../../../utils/request'); + +Page({ + data: { + stats: { inviteCount: 0, teamCount: 0, totalCommission: '0', settledCommission: '0', pendingCommission: '0', orderCount: 0, promotionCode: '' }, + loading: true + }, + + onShow() { this.loadStats(); }, + + onPullDownRefresh() { this.loadStats().finally(function() { wx.stopPullDownRefresh(); }); }, + + async loadStats() { + this.setData({ loading: true }); + try { + var res = await request({ url: '/system/promotion/myStats', method: 'GET', loading: false }); + if (res.code === 200 && res.data) { + this.setData({ stats: res.data, loading: false }); + } + } catch (e) { this.setData({ loading: false }); } + }, + + goTeam() { wx.navigateTo({ url: '/subPackages/pages-promoter/team/team' }); }, + goCommission() { wx.navigateTo({ url: '/subPackages/pages-promoter/commission/commission' }); }, + goShare() { wx.navigateTo({ url: '/subPackages/pages-promoter/share/share' }); } +}); diff --git a/coupon/subPackages/pages-promoter/dashboard/dashboard.json b/coupon/subPackages/pages-promoter/dashboard/dashboard.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/coupon/subPackages/pages-promoter/dashboard/dashboard.json @@ -0,0 +1 @@ +{} diff --git a/coupon/subPackages/pages-promoter/dashboard/dashboard.wxml b/coupon/subPackages/pages-promoter/dashboard/dashboard.wxml new file mode 100644 index 0000000..61a159e --- /dev/null +++ b/coupon/subPackages/pages-promoter/dashboard/dashboard.wxml @@ -0,0 +1,51 @@ + + + + + + + + + {{stats.inviteCount || 0}} + 直接邀请 + + + {{stats.teamCount || 0}} + 团队总人数 + + + ¥{{stats.totalCommission || '0'}} + 佣金总额 + + + {{stats.orderCount || 0}} + 推广订单 + + + + + + 已结算佣金 + ¥{{stats.settledCommission || '0'}} + + + 待结算佣金 + ¥{{stats.pendingCommission || '0'}} + + + + + + 👥 + 我的团队 + + + 💰 + 佣金明细 + + + 📤 + 分享推广 + + + diff --git a/coupon/subPackages/pages-promoter/dashboard/dashboard.wxss b/coupon/subPackages/pages-promoter/dashboard/dashboard.wxss new file mode 100644 index 0000000..a3050b1 --- /dev/null +++ b/coupon/subPackages/pages-promoter/dashboard/dashboard.wxss @@ -0,0 +1,24 @@ +.container { padding: 0 0 40rpx 0; background: #F5F5F5; min-height: 100vh; } +.top-banner { background: linear-gradient(135deg, #FF6A6A, #FF8F1F); color: #fff; padding: 48rpx 32rpx 40rpx; } +.banner-title { font-size: 36rpx; font-weight: 800; } +.banner-code { font-size: 24rpx; margin-top: 8rpx; opacity: 0.85; } + +.stats-grid { display: flex; flex-wrap: wrap; margin: -20rpx 24rpx 0; } +.stat-card { width: 44%; margin: 8rpx 3%; background: #fff; border-radius: 16rpx; padding: 28rpx 24rpx; text-align: center; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); } +.stat-num { font-size: 40rpx; font-weight: 800; color: #333; } +.stat-num.gold { color: #FF8F1F; } +.stat-label { font-size: 22rpx; color: #999; margin-top: 8rpx; } + +.income-summary { margin: 24rpx 32rpx; background: #fff; border-radius: 16rpx; padding: 24rpx 32rpx; } +.income-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #F5F5F5; } +.income-row:last-child { border-bottom: none; } +.income-label { font-size: 28rpx; color: #333; } +.income-val { font-size: 28rpx; font-weight: 700; } +.income-val.green { color: #67C23A; } +.income-val.orange { color: #E6A23C; } + +.action-grid { display: flex; margin: 24rpx 32rpx; } +.action-item { flex: 1; background: #fff; border-radius: 16rpx; padding: 32rpx 0; text-align: center; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); } +.action-item + .action-item { margin-left: 16rpx; } +.action-icon { font-size: 44rpx; } +.action-text { font-size: 24rpx; color: #333; margin-top: 12rpx; display: block; } diff --git a/coupon/subPackages/pages-promoter/share/share.js b/coupon/subPackages/pages-promoter/share/share.js new file mode 100644 index 0000000..40420b1 --- /dev/null +++ b/coupon/subPackages/pages-promoter/share/share.js @@ -0,0 +1,54 @@ +var { request } = require('../../../utils/request'); +var app = getApp(); + +Page({ + data: { + promotionCode: '', + loading: false + }, + + onShow() { this.loadCode(); }, + + async loadCode() { + try { + var res = await request({ url: '/system/promotion/myStats', method: 'GET', loading: false }); + if (res.code === 200 && res.data) { + this.setData({ promotionCode: res.data.promotionCode || '' }); + } + } catch (e) {} + }, + + // 生成/刷新邀请码 + async generateCode() { + this.setData({ loading: true }); + try { + var res = await request({ url: '/system/promotion/generateCode', method: 'POST', loading: false }); + if (res.code === 200) { + this.setData({ promotionCode: res.data || '' }); + wx.showToast({ title: '邀请码已生成', icon: 'success' }); + } + } catch (e) {} + this.setData({ loading: false }); + }, + + // 复制邀请码 + copyCode() { + if (!this.data.promotionCode) { + wx.showToast({ title: '请先生成邀请码', icon: 'none' }); + return; + } + wx.setClipboardData({ + data: this.data.promotionCode, + success: function() { wx.showToast({ title: '邀请码已复制', icon: 'success' }); } + }); + }, + + // 分享消息卡片 + onShareAppMessage() { + return { + title: '加入瑞莱健康陪诊平台,一起赚佣金!', + path: '/pages/index/index?promotionCode=' + this.data.promotionCode, + imageUrl: '/pages/image/home/home-banner.png' + }; + } +}); diff --git a/coupon/subPackages/pages-promoter/share/share.json b/coupon/subPackages/pages-promoter/share/share.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/coupon/subPackages/pages-promoter/share/share.json @@ -0,0 +1 @@ +{} diff --git a/coupon/subPackages/pages-promoter/share/share.wxml b/coupon/subPackages/pages-promoter/share/share.wxml new file mode 100644 index 0000000..4522380 --- /dev/null +++ b/coupon/subPackages/pages-promoter/share/share.wxml @@ -0,0 +1,45 @@ + + + + + 我的邀请码 + + {{promotionCode || '暂未生成'}} + + + + + + + + + + + 推广佣金规则 + 一级推广订单金额的 5% + 二级推广订单金额的 3% + 三级推广订单金额的 2% + + diff --git a/coupon/subPackages/pages-promoter/share/share.wxss b/coupon/subPackages/pages-promoter/share/share.wxss new file mode 100644 index 0000000..9605aac --- /dev/null +++ b/coupon/subPackages/pages-promoter/share/share.wxss @@ -0,0 +1,27 @@ +.container { padding: 0 0 60rpx 0; background: #F5F5F5; min-height: 100vh; } +.share-banner { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; padding: 48rpx 32rpx; text-align: center; } +.banner-text { font-size: 36rpx; font-weight: 800; } +.banner-sub { font-size: 24rpx; margin-top: 12rpx; opacity: 0.85; } + +.code-section { margin: 24rpx 32rpx; background: #fff; border-radius: 16rpx; padding: 32rpx; text-align: center; } +.code-label { font-size: 28rpx; color: #666; margin-bottom: 16rpx; } +.code-box { background: #F5F5F5; border-radius: 12rpx; padding: 24rpx; margin-bottom: 24rpx; } +.code-value { font-size: 48rpx; font-weight: 800; color: #333; letter-spacing: 8rpx; } +.code-actions { display: flex; justify-content: center; gap: 16rpx; } +.gen-btn { font-size: 26rpx; padding: 14rpx 28rpx; border-radius: 32rpx; background: #FF8F1F; color: #fff; border: none; line-height: 1.4; } +.gen-btn.disabled { background: #ccc; } +.copy-btn { font-size: 26rpx; padding: 14rpx 28rpx; border-radius: 32rpx; background: #fff; color: #FF8F1F; border: 1rpx solid #FF8F1F; line-height: 1.4; } + +.share-section { margin: 0 32rpx 24rpx; background: #fff; border-radius: 16rpx; padding: 32rpx; } +.section-title { font-size: 30rpx; font-weight: 700; color: #333; margin-bottom: 20rpx; } +.share-tips { margin-bottom: 28rpx; } +.tip-item { display: flex; align-items: center; margin-bottom: 16rpx; } +.tip-icon { width: 36rpx; height: 36rpx; background: #FF8F1F; color: #fff; border-radius: 50%; text-align: center; line-height: 36rpx; font-size: 22rpx; margin-right: 12rpx; } +.tip-text { font-size: 26rpx; color: #666; } +.share-btn { width: 100%; background: #07C160; color: #fff; border-radius: 44rpx; font-size: 30rpx; padding: 20rpx 0; border: none; } + +.commission-rules { margin: 0 32rpx; background: #fff; border-radius: 16rpx; padding: 32rpx; } +.rule-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #F5F5F5; } +.rule-row:last-child { border-bottom: none; } +.rule-lv { font-size: 28rpx; color: #333; } +.rule-pct { font-size: 28rpx; color: #FF8F1F; font-weight: 600; } diff --git a/coupon/subPackages/pages-promoter/team/team.js b/coupon/subPackages/pages-promoter/team/team.js new file mode 100644 index 0000000..59fd135 --- /dev/null +++ b/coupon/subPackages/pages-promoter/team/team.js @@ -0,0 +1,22 @@ +var { request } = require('../../../utils/request'); + +Page({ + data: { + members: [], + loading: true + }, + + onShow() { this.loadTeam(); }, + + onPullDownRefresh() { this.loadTeam().finally(function() { wx.stopPullDownRefresh(); }); }, + + async loadTeam() { + this.setData({ loading: true }); + try { + var res = await request({ url: '/system/promotion/team', method: 'GET', loading: false }); + if (res.code === 200) { + this.setData({ members: res.data || [], loading: false }); + } + } catch (e) { this.setData({ loading: false }); } + } +}); diff --git a/coupon/subPackages/pages-promoter/team/team.json b/coupon/subPackages/pages-promoter/team/team.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/coupon/subPackages/pages-promoter/team/team.json @@ -0,0 +1 @@ +{} diff --git a/coupon/subPackages/pages-promoter/team/team.wxml b/coupon/subPackages/pages-promoter/team/team.wxml new file mode 100644 index 0000000..55c9a38 --- /dev/null +++ b/coupon/subPackages/pages-promoter/team/team.wxml @@ -0,0 +1,33 @@ + + 我的团队共{{members.length}}人 + + 加载中... + + + + + {{item.nickName || '用户' + item.userId}} + {{item.phonenumber}} + + + + {{item.inviteCount || 0}} + 邀请 + + + ¥{{item.commission || '0'}} + 佣金贡献 + + + L{{item.level}} + + + 加入于 {{item.joinTime}} + + + + + 暂无团队成员 + 分享您的邀请码,邀请好友加入推广 + + diff --git a/coupon/subPackages/pages-promoter/team/team.wxss b/coupon/subPackages/pages-promoter/team/team.wxss new file mode 100644 index 0000000..2f77f50 --- /dev/null +++ b/coupon/subPackages/pages-promoter/team/team.wxss @@ -0,0 +1,22 @@ +.container { padding: 24rpx 32rpx; background: #F5F5F5; min-height: 100vh; } +.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; } +.title { font-size: 32rpx; font-weight: 700; color: #333; } +.count { font-size: 24rpx; color: #999; } + +.loading { text-align: center; padding: 80rpx; color: #999; font-size: 28rpx; } + +.member-card { background: #fff; border-radius: 16rpx; padding: 28rpx 32rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); } +.member-info { display: flex; align-items: center; } +.member-name { font-size: 30rpx; font-weight: 600; color: #333; } +.member-phone { font-size: 24rpx; color: #999; margin-left: 16rpx; } + +.member-stats { display: flex; margin-top: 20rpx; padding-top: 20rpx; border-top: 1rpx solid #F5F5F5; } +.member-stat { flex: 1; text-align: center; } +.ms-num { font-size: 32rpx; font-weight: 700; color: #333; display: block; } +.ms-num.gold { color: #FF8F1F; } +.ms-label { font-size: 22rpx; color: #999; } + +.member-time { font-size: 22rpx; color: #C0C4CC; margin-top: 12rpx; } + +.empty { text-align: center; padding: 120rpx 40rpx; color: #999; font-size: 28rpx; } +.empty-hint { display: block; font-size: 24rpx; color: #C0C4CC; margin-top: 16rpx; } diff --git a/coupon/subPackages/pages-work/dispatch/dispatch.js b/coupon/subPackages/pages-work/dispatch/dispatch.js new file mode 100644 index 0000000..d0bd46f --- /dev/null +++ b/coupon/subPackages/pages-work/dispatch/dispatch.js @@ -0,0 +1,91 @@ +var { request } = require('../../../utils/request'); +var app = getApp(); + +Page({ + data: { + orders: [], + caregivers: [], + orderLoading: false, + selectedOrder: null, + selectedCaregiverId: null, + showDialog: false, + history: [], + historyLoading: false + }, + + onShow() { + this.loadPending(); + this.loadCaregivers(); + this.loadHistory(); + }, + + onPullDownRefresh() { + Promise.all([this.loadPending(), this.loadCaregivers(), this.loadHistory()]) + .finally(function() { wx.stopPullDownRefresh(); }); + }, + + async loadPending() { + this.setData({ orderLoading: true }); + try { + var res = await request({ url: '/system/dispatch/pending', method: 'GET', loading: false }); + if (res.code === 200) { + this.setData({ orders: res.data || [], orderLoading: false }); + } + } catch (e) { this.setData({ orderLoading: false }); } + }, + + async loadCaregivers() { + try { + var res = await request({ url: '/system/dispatch/caregivers', method: 'GET', loading: false }); + if (res.code === 200) { + this.setData({ caregivers: res.data || [] }); + } + } catch (e) {} + }, + + async loadHistory() { + this.setData({ historyLoading: true }); + try { + var res = await request({ url: '/system/dispatch/history', method: 'GET', loading: false }); + if (res.code === 200) { + this.setData({ history: res.data || [], historyLoading: false }); + } + } catch (e) { this.setData({ historyLoading: false }); } + }, + + showAssignDialog(e) { + var order = e.currentTarget.dataset.item; + this.setData({ selectedOrder: order, selectedCaregiverId: null, showDialog: true }); + }, + + async doAssign() { + if (!this.data.selectedCaregiverId || !this.data.selectedOrder) return; + try { + var res = await request({ + url: '/system/dispatch/assign/' + this.data.selectedOrder.orderId + '/' + this.data.selectedCaregiverId, + method: 'POST', + loading: true, + loadingText: '派单中...' + }); + if (res.code === 200) { + wx.showToast({ title: '派单成功', icon: 'success' }); + this.setData({ showDialog: false }); + this.loadPending(); + this.loadCaregivers(); + this.loadHistory(); + } + } catch (e) {} + }, + + selectCaregiver(e) { + this.setData({ selectedCaregiverId: e.currentTarget.dataset.id }); + }, + + closeDialog() { + this.setData({ showDialog: false, selectedOrder: null, selectedCaregiverId: null }); + }, + + getStatusLabel(s) { + return app.getStatusLabel(s); + } +}); diff --git a/coupon/subPackages/pages-work/dispatch/dispatch.json b/coupon/subPackages/pages-work/dispatch/dispatch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/coupon/subPackages/pages-work/dispatch/dispatch.json @@ -0,0 +1 @@ +{} diff --git a/coupon/subPackages/pages-work/dispatch/dispatch.wxml b/coupon/subPackages/pages-work/dispatch/dispatch.wxml new file mode 100644 index 0000000..1a9c21e --- /dev/null +++ b/coupon/subPackages/pages-work/dispatch/dispatch.wxml @@ -0,0 +1,54 @@ + + 待派单订单 + + 加载中... + + + + + #{{item.yuliu1 || item.orderId}} + 医院{{item.hospitalId}} + ¥{{item.yuguMoney}} + + + {{item.createTime}} + {{item.status === '0' ? '待接单' : item.status}} + + + + + + + + 暂无待派单订单 + + + + + 选择陪诊师 + + + {{item.nickName || '未命名'}} + {{item.phonenumber}} + + + + + + + + + + + 我的派单记录 + 加载中... + + + #{{item.yuliu1 || item.orderId}} + 陪诊师{{item.bId}} + ¥{{item.yuguMoney}} + {{item.status === '1' ? '已接单' : item.status === '4' ? '已完成' : item.status}} + + + 暂无记录 + diff --git a/coupon/subPackages/pages-work/dispatch/dispatch.wxss b/coupon/subPackages/pages-work/dispatch/dispatch.wxss new file mode 100644 index 0000000..de2a501 --- /dev/null +++ b/coupon/subPackages/pages-work/dispatch/dispatch.wxss @@ -0,0 +1,35 @@ +.container { padding: 24rpx 32rpx; background: #F5F5F5; min-height: 100vh; } +.section-title { font-size: 30rpx; font-weight: 700; color: #333; margin: 24rpx 0 16rpx; } +.loading { text-align: center; padding: 60rpx; color: #999; } +.empty { text-align: center; padding: 80rpx; color: #999; font-size: 28rpx; } + +.order-card { background: #fff; border-radius: 16rpx; padding: 24rpx 28rpx; margin-bottom: 12rpx; } +.order-info { display: flex; justify-content: space-between; align-items: center; } +.order-no { font-size: 28rpx; font-weight: 600; color: #333; } +.order-hospital { font-size: 24rpx; color: #666; } +.order-amount { font-size: 30rpx; font-weight: 700; color: #E6A23C; } +.order-meta { display: flex; justify-content: space-between; margin-top: 12rpx; } +.order-time { font-size: 22rpx; color: #999; } +.order-status { font-size: 22rpx; color: #FF9800; } +.order-action { text-align: right; margin-top: 16rpx; } +.assign-btn { display: inline-block; font-size: 24rpx; padding: 8rpx 24rpx; background: #1890FF; color: #fff; border-radius: 32rpx; border: none; line-height: 1.4; } + +/* 弹窗 */ +.dialog-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 100; display: flex; justify-content: center; align-items: center; } +.dialog-box { width: 600rpx; max-height: 80vh; background: #fff; border-radius: 16rpx; overflow: hidden; } +.dialog-title { font-size: 32rpx; font-weight: 700; text-align: center; padding: 32rpx 0 16rpx; } +.caregiver-list { max-height: 500rpx; padding: 0 24rpx; } +.caregiver-item { display: flex; justify-content: space-between; padding: 20rpx 16rpx; border-bottom: 1rpx solid #F5F5F5; } +.caregiver-item.selected { background: #E6F7FF; border-radius: 8rpx; } +.cg-name { font-size: 28rpx; color: #333; } +.cg-phone { font-size: 24rpx; color: #999; } +.dialog-buttons { display: flex; padding: 20rpx 24rpx; gap: 16rpx; } +.btn-cancel { flex: 1; font-size: 28rpx; padding: 16rpx 0; border-radius: 8rpx; background: #F5F5F5; color: #666; border: none; } +.btn-confirm { flex: 1; font-size: 28rpx; padding: 16rpx 0; border-radius: 8rpx; background: #1890FF; color: #fff; border: none; } + +/* 历史 */ +.history-item { display: flex; justify-content: space-between; align-items: center; background: #fff; padding: 20rpx 28rpx; margin-bottom: 8rpx; border-radius: 8rpx; } +.h-order { font-size: 26rpx; color: #333; } +.h-caregiver { font-size: 24rpx; color: #999; } +.h-amount { font-size: 26rpx; color: #E6A23C; } +.h-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; background: #E8F5E9; color: #4CAF50; } diff --git a/coupon/subPackages/pages-work/ops/ops.js b/coupon/subPackages/pages-work/ops/ops.js new file mode 100644 index 0000000..1fe916d --- /dev/null +++ b/coupon/subPackages/pages-work/ops/ops.js @@ -0,0 +1,44 @@ +var { request } = require('../../../utils/request'); + +Page({ + data: { + totalIncome: '--', + totalRefund: '--', + totalOrders: '--', + netIncome: '--', + trend: [], + loading: true + }, + + onShow() { this.loadStats(); }, + + onPullDownRefresh() { this.loadStats().finally(function() { wx.stopPullDownRefresh(); }); }, + + async loadStats() { + this.setData({ loading: true }); + try { + // Get current month date range + var now = new Date(); + var endDate = now.toISOString().split('T')[0]; + var startDate = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0]; + + var res = await request({ + url: '/system/view/incomeStats', + method: 'GET', + data: { startDate: startDate, endDate: endDate, groupBy: 'day' }, + loading: false + }); + if (res.code === 200 && res.data) { + var d = res.data; + this.setData({ + totalIncome: d.totalIncome || '0.00', + totalRefund: d.totalRefund || '0.00', + totalOrders: d.totalOrders || 0, + netIncome: ((parseFloat(d.totalIncome) || 0) - (parseFloat(d.totalRefund) || 0)).toFixed(2), + trend: d.trend || [] + }); + } + } catch (e) {} + this.setData({ loading: false }); + } +}); diff --git a/coupon/subPackages/pages-work/ops/ops.json b/coupon/subPackages/pages-work/ops/ops.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/coupon/subPackages/pages-work/ops/ops.json @@ -0,0 +1 @@ +{} diff --git a/coupon/subPackages/pages-work/ops/ops.wxml b/coupon/subPackages/pages-work/ops/ops.wxml new file mode 100644 index 0000000..8910da2 --- /dev/null +++ b/coupon/subPackages/pages-work/ops/ops.wxml @@ -0,0 +1,42 @@ + + + 运营看板 + 本月数据概览 + + + 加载中... + + + + ¥{{totalIncome}} + 总收入 + + + ¥{{totalRefund}} + 退款总额 + + + ¥{{netIncome}} + 净收入 + + + {{totalOrders}} + 总订单数 + + + + + 每日趋势 + + + + {{item.date}} + ¥{{item.income || '0'}} + {{item.orderCount || 0}}单 + + + + + + 详细数据请登录管理后台查看 + diff --git a/coupon/subPackages/pages-work/ops/ops.wxss b/coupon/subPackages/pages-work/ops/ops.wxss new file mode 100644 index 0000000..f793251 --- /dev/null +++ b/coupon/subPackages/pages-work/ops/ops.wxss @@ -0,0 +1,27 @@ +.container { padding: 0 0 40rpx 0; background: #F5F5F5; min-height: 100vh; } + +/* === header === */ +.cg-header { background: #fff; padding: 32rpx 40rpx; margin-bottom: 16rpx; } +.cg-header-title { font-size: 36rpx; font-weight: bold; color: #333; display: block; } +.cg-header-sub { font-size: 24rpx; color: #999; margin-top: 8rpx; } + +/* === stats grid === */ +.stats-grid { display: flex; flex-wrap: wrap; margin: 0 24rpx; } +.stat-card { width: 44%; margin: 8rpx 3%; background: #fff; border-radius: 16rpx; padding: 28rpx 24rpx; text-align: center; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); } +.stat-num { font-size: 40rpx; font-weight: 800; color: #333; } +.stat-num.gold { color: #FF8F1F; } +.stat-num.red { color: #F56C6C; } +.stat-label { font-size: 22rpx; color: #999; margin-top: 8rpx; } + +/* === trend === */ +.trend-section { margin: 24rpx 32rpx; } +.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 12rpx; } +.trend-list { background: #fff; border-radius: 16rpx; padding: 8rpx 0; } +.trend-item { display: flex; justify-content: space-between; padding: 16rpx 32rpx; border-bottom: 1rpx solid #F5F5F5; } +.trend-item:last-child { border-bottom: none; } +.trend-date { font-size: 24rpx; color: #999; } +.trend-income { font-size: 26rpx; font-weight: 600; color: #67C23A; } +.trend-orders { font-size: 24rpx; color: #999; } + +.loading { text-align: center; padding: 40rpx; color: #999; } +.hint { text-align: center; margin-top: 24rpx; font-size: 24rpx; color: #C0C4CC; } diff --git a/coupon/subPackages/pages-work/refund/refund.js b/coupon/subPackages/pages-work/refund/refund.js new file mode 100644 index 0000000..e973cbd --- /dev/null +++ b/coupon/subPackages/pages-work/refund/refund.js @@ -0,0 +1,73 @@ +var { request } = require('../../../utils/request'); + +Page({ + data: { + orders: [], + loading: false, + reason: '', + showReject: false, + currentOrderId: null + }, + + onShow() { this.loadOrders(); }, + + onPullDownRefresh() { this.loadOrders().finally(function() { wx.stopPullDownRefresh(); }); }, + + async loadOrders() { + this.setData({ loading: true }); + try { + var res = await request({ + url: '/system/view/list', + method: 'GET', + loading: false, + data: { pageNum: 1, pageSize: 50, status: '5' } + }); + if (res.code === 200) { + this.setData({ orders: (res.rows || res.data || []), loading: false }); + } + } catch (e) { this.setData({ loading: false }); } + }, + + showRejectDialog(e) { + var id = e.currentTarget.dataset.id; + this.setData({ showReject: true, currentOrderId: id, reason: '' }); + }, + + async doApprove(e) { + var orderId = e.currentTarget.dataset.id; + await this.submitReview(orderId, true, ''); + }, + + async submitRefuse() { + if (!this.data.reason.trim()) { + wx.showToast({ title: '请输入拒绝原因', icon: 'none' }); + return; + } + await this.submitReview(this.data.currentOrderId, false, this.data.reason); + }, + + onReasonInput(e) { + this.setData({ reason: e.detail.value }); + }, + + closeReject() { + this.setData({ showReject: false, currentOrderId: null, reason: '' }); + }, + + async submitReview(orderId, approved, reason) { + try { + var res = await request({ + url: '/system/view/refundApprove/' + orderId, + method: 'PUT', + data: { approved: approved, reason: reason }, + loading: true, + loadingText: approved ? '通过中...' : '拒绝中...' + }); + if (res.code === 200) { + wx.showToast({ title: approved ? '已通过' : '已拒绝', icon: 'success' }); + this.setData({ showReject: false }); + this.loadOrders(); + } + } catch (e) {} + } +}); diff --git a/coupon/subPackages/pages-work/refund/refund.json b/coupon/subPackages/pages-work/refund/refund.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/coupon/subPackages/pages-work/refund/refund.json @@ -0,0 +1 @@ +{} diff --git a/coupon/subPackages/pages-work/refund/refund.wxml b/coupon/subPackages/pages-work/refund/refund.wxml new file mode 100644 index 0000000..0304538 --- /dev/null +++ b/coupon/subPackages/pages-work/refund/refund.wxml @@ -0,0 +1,37 @@ + + 退款审核 + + 加载中... + + + + + 订单 #{{item.yuliu1 || item.orderId}} + ¥{{item.yuguMoney || item.orderMoney}} + + + 患者ID: {{item.cId}} + 申请退款 + + 原因: {{item.remark}} + + + + + + + + 暂无退款申请 + + + + + 拒绝原因 + + + + + + + + diff --git a/coupon/subPackages/pages-work/refund/refund.wxss b/coupon/subPackages/pages-work/refund/refund.wxss new file mode 100644 index 0000000..848bfdc --- /dev/null +++ b/coupon/subPackages/pages-work/refund/refund.wxss @@ -0,0 +1,24 @@ +.container { padding: 24rpx 32rpx; background: #F5F5F5; min-height: 100vh; } +.section-title { font-size: 30rpx; font-weight: 700; color: #333; margin-bottom: 16rpx; } +.loading { text-align: center; padding: 80rpx; color: #999; } +.empty { text-align: center; padding: 120rpx; color: #999; font-size: 28rpx; } + +.refund-card { background: #fff; border-radius: 16rpx; padding: 24rpx 28rpx; margin-bottom: 12rpx; } +.refund-row { display: flex; justify-content: space-between; align-items: center; } +.refund-row.sub { margin-top: 10rpx; } +.r-order { font-size: 28rpx; font-weight: 600; color: #333; } +.r-amount { font-size: 30rpx; font-weight: 700; color: #E6A23C; } +.r-meta { font-size: 24rpx; color: #666; } +.r-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; background: #FFF3E0; color: #FF9800; } +.refund-reason { font-size: 24rpx; color: #999; margin-top: 12rpx; padding-top: 12rpx; border-top: 1rpx solid #F5F5F5; } +.refund-actions { display: flex; justify-content: flex-end; gap: 16rpx; margin-top: 16rpx; } +.btn-approve { font-size: 24rpx; padding: 10rpx 28rpx; background: #4CAF50; color: #fff; border-radius: 32rpx; border: none; line-height: 1.4; } +.btn-reject { font-size: 24rpx; padding: 10rpx 28rpx; background: #F56C6C; color: #fff; border-radius: 32rpx; border: none; line-height: 1.4; } + +.dialog-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 100; display: flex; justify-content: center; align-items: center; } +.dialog-box { width: 600rpx; background: #fff; border-radius: 16rpx; padding: 32rpx; } +.dialog-title { font-size: 32rpx; font-weight: 700; text-align: center; margin-bottom: 24rpx; } +.reason-input { width: 100%; height: 160rpx; background: #F5F5F5; border-radius: 12rpx; padding: 16rpx; font-size: 26rpx; box-sizing: border-box; } +.dialog-buttons { display: flex; gap: 16rpx; margin-top: 24rpx; } +.btn-cancel { flex: 1; font-size: 28rpx; padding: 16rpx 0; border-radius: 8rpx; background: #F5F5F5; color: #666; border: none; } +.btn-confirm { flex: 1; font-size: 28rpx; padding: 16rpx 0; border-radius: 8rpx; background: #F56C6C; color: #fff; border: none; } diff --git a/coupon/subPackages/pages-work/search/search.js b/coupon/subPackages/pages-work/search/search.js new file mode 100644 index 0000000..f8ce745 --- /dev/null +++ b/coupon/subPackages/pages-work/search/search.js @@ -0,0 +1,81 @@ +var { request } = require('../../../utils/request'); +var app = getApp(); + +Page({ + data: { + orders: [], + keyword: '', + statusFilter: '', + pageNum: 1, + hasMore: true, + loading: false, + firstLoad: true, + statusOptions: [ + { label: '全部', value: '' }, + { label: '待接单', value: '0' }, + { label: '已接单', value: '1' }, + { label: '服务中', value: '2' }, + { label: '待支付', value: '3' }, + { label: '已完成', value: '4' }, + { label: '申请退款', value: '5' }, + { label: '退款中', value: '6' }, + { label: '已退款', value: '7' }, + { label: '已结算', value: '8' } + ] + }, + + onShow() { + if (this.data.firstLoad) { + this.setData({ firstLoad: false }); + this.searchOrders(); + } + }, + + onReachBottom() { + if (this.data.hasMore && !this.data.loading) this.searchOrders(); + }, + + onPullDownRefresh() { + this.setData({ pageNum: 1, orders: [] }); + this.searchOrders().finally(function() { wx.stopPullDownRefresh(); }); + }, + + onSearchInput(e) { this.setData({ keyword: e.detail.value }); }, + + onStatusTap(e) { + this.setData({ statusFilter: e.currentTarget.dataset.status, pageNum: 1, orders: [] }); + this.searchOrders(); + }, + + doSearch() { + this.setData({ pageNum: 1, orders: [] }); + this.searchOrders(); + }, + + async searchOrders() { + if (this.data.loading) return; + this.setData({ loading: true }); + try { + var params = { pageNum: this.data.pageNum, pageSize: 20 }; + if (this.data.keyword) params.keyword = this.data.keyword; + if (this.data.statusFilter) params.status = this.data.statusFilter; + var res = await request({ url: '/system/view/list', method: 'GET', data: params, loading: false }); + if (res.code === 200) { + var list = res.rows || res.data || []; + if (this.data.pageNum === 1) { + this.setData({ orders: list, hasMore: list.length >= 20, pageNum: 2 }); + } else { + this.setData({ orders: this.data.orders.concat(list), hasMore: list.length >= 20, pageNum: this.data.pageNum + 1 }); + } + } + } catch (e) {} + this.setData({ loading: false }); + }, + + getStatusLabel(s) { return app.getStatusLabel(s); }, + + goDetail(e) { + var item = JSON.stringify(e.currentTarget.dataset.item); + wx.navigateTo({ url: '/pages/orderdetail/orderdetail?item=' + item }); + } +}); diff --git a/coupon/subPackages/pages-work/search/search.json b/coupon/subPackages/pages-work/search/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/coupon/subPackages/pages-work/search/search.json @@ -0,0 +1 @@ +{} diff --git a/coupon/subPackages/pages-work/search/search.wxml b/coupon/subPackages/pages-work/search/search.wxml new file mode 100644 index 0000000..e0f6d44 --- /dev/null +++ b/coupon/subPackages/pages-work/search/search.wxml @@ -0,0 +1,35 @@ + + + + + + + + + {{item.label}} + + + + + + + {{item.yuliu1 || '#' + item.orderId}} + ¥{{item.yuguMoney || item.orderMoney}} + + + 患者ID: {{item.cId}} | 陪诊师ID: {{item.bId || '--'}} + + + {{item.createTime}} + {{item.status === '-2' ? '已取消' : item.status === '-1' ? '拒绝接单' : item.status === '0' ? '待接单' : item.status === '1' ? '已接单' : item.status === '2' ? '服务中' : item.status === '3' ? '待支付' : item.status === '4' ? '已完成' : item.status === '5' ? '申请退款' : item.status === '6' ? '退款中' : item.status === '7' ? '已退款' : item.status === '8' ? '已结算' : item.status}} + + + + + + 未找到订单 + + + 加载中... + — 已加载全部 — + diff --git a/coupon/subPackages/pages-work/search/search.wxss b/coupon/subPackages/pages-work/search/search.wxss new file mode 100644 index 0000000..c16bd59 --- /dev/null +++ b/coupon/subPackages/pages-work/search/search.wxss @@ -0,0 +1,21 @@ +.container { padding: 0; background: #F5F5F5; min-height: 100vh; } +.search-bar { display: flex; padding: 16rpx 24rpx; background: #fff; } +.search-input { flex: 1; height: 68rpx; background: #F5F5F5; border-radius: 34rpx; padding: 0 28rpx; font-size: 26rpx; } +.search-btn { width: 120rpx; height: 68rpx; line-height: 68rpx; text-align: center; font-size: 26rpx; background: #1890FF; color: #fff; border-radius: 34rpx; border: none; margin-left: 12rpx; padding: 0; } + +.status-bar { white-space: nowrap; padding: 16rpx 24rpx; background: #fff; border-top: 1rpx solid #F5F5F5; } +.status-item { display: inline-block; font-size: 24rpx; padding: 10rpx 20rpx; margin-right: 12rpx; border-radius: 24rpx; background: #F5F5F5; color: #666; } +.status-item.active { background: #1890FF; color: #fff; } + +.order-card { background: #fff; margin: 12rpx 0 0 0; padding: 24rpx 32rpx; } +.o-row { display: flex; justify-content: space-between; align-items: center; } +.o-row.sub { margin-top: 8rpx; } +.o-no { font-size: 28rpx; font-weight: 600; color: #333; } +.o-amount { font-size: 30rpx; font-weight: 700; color: #E6A23C; } +.o-meta { font-size: 24rpx; color: #999; } +.o-time { font-size: 22rpx; color: #C0C4CC; } +.o-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; background: #E8F5E9; color: #4CAF50; } + +.loading { text-align: center; padding: 40rpx; color: #999; } +.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 28rpx; } +.end { text-align: center; padding: 32rpx; color: #C0C4CC; font-size: 24rpx; } diff --git a/docker-compose.yml b/docker-compose.yml index cab68fa..fc32a49 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,22 @@ services: MYSQL_PASSWORD: ${MYSQL_PASSWORD} MYSQL_USE_SSL: ${MYSQL_USE_SSL:-true} RUOYI_PROFILE: /data/ruoyi/upload + # 安全配置(生产环境务必设置强随机值) + TOKEN_SECRET: ${TOKEN_SECRET} + SWAGGER_ENABLED: "false" + DRUID_MONITOR_ENABLED: "false" + DRUID_ALLOW_IP: "127.0.0.1" + DRUID_PASSWORD: ${DRUID_PASSWORD:-} + # 微信支付配置(通过 .env 注入) + WX_APP_ID: ${WX_APP_ID} + WX_SECRET: ${WX_SECRET} + WX_MCH_ID: ${WX_MCH_ID} + WX_MCH_SERIAL_NO: ${WX_MCH_SERIAL_NO} + WX_API_V3_KEY: ${WX_API_V3_KEY} + WX_PRIVATE_KEY: ${WX_PRIVATE_KEY} + WX_NOTIFY_URL: ${WX_NOTIFY_URL} + WX_REFUND_NOTIFY_URL: ${WX_REFUND_NOTIFY_URL} + AES_ENCRYPT_KEY: ${AES_ENCRYPT_KEY:-baiyangdianzicom} depends_on: redis: condition: service_healthy diff --git a/docs/AI大模型赋能与未来演进.md b/docs/AI大模型赋能与未来演进.md new file mode 100644 index 0000000..f82dfbb --- /dev/null +++ b/docs/AI大模型赋能与未来演进.md @@ -0,0 +1,310 @@ +# 瑞来健康 — AI 大模型赋能与未来演进 + +> 思考角度:多角色系统落地后,引入 AI 大模型可以从哪些维度进一步提升竞争力 +> 日期:2026-06-07 + +--- + +## 一、AI 能力的定位:不是替换人,是放大人的效率 + +当前平台有 7 个角色,每个人做的事情中,有相当比例是"重复性、规则性、信息密集型"的工作——这正是 AI 最擅长替代的。 + +``` +AI 不做的事: + ✗ 陪诊师去医院陪护患者(物理世界) + ✗ 管理者做战略决策(需要商业判断) + +AI 最适合做的事: + ✓ 回复重复性问题(客服) + ✓ 匹配订单与陪诊师(调度) + ✓ 审核认证材料(运营) + ✓ 分析数据给出建议(管理) + ✓ 生成推广文案/海报(推广) + ✓ 风险预警与异常检测(安全) +``` + +--- + +## 二、分角色的 AI 赋能场景 + +### 2.1 智能客服 — 提升最大、见效最快 + +**当前痛点**:客服专员需要人工回复大量重复性问题(退款规则、服务时间、医院地址、订单状态查询),效率低且响应慢。 + +**AI 赋能后**: + +| 能力 | 说明 | 价值 | +|------|------|------| +| **7×24 自动应答** | 常见问题秒回:服务价格、退款政策、医院导航、订单查询 | 客服效率 10× | +| **意图识别+路由** | 用户说"我要退款" → AI 判断意图 → 自动引导填写原因 → 生成工单 → 推送给客服审核 | 减少 80% 重复劳动 | +| **情绪感知+升级** | 检测到用户愤怒/紧急(AI 语义分析)→ 自动标记优先级 + 通知人工客服介入 | 避免投诉升级 | +| **多轮对话** | "帮我查我昨天的订单" → AI 查库 → "订单已在服务中,陪诊师王XX,预计18:00完成" | 用户自助率 60%+ | +| **知识库问答** | 接入医院数据库、陪诊流程文档,回答"XX医院怎么挂号?""三甲医院专家出诊时间?" | 从"等待回复"到"即时解答" | +| **工单自动总结** | 客服处理完纠纷 → AI 生成工单摘要 + 处理建议归档 | 知识沉淀 | + +**落地路径**: + +``` +阶段 1(2周):小程序内嵌 AI 聊天入口 + → 对接 LLM API + 平台知识库(RAG) + → 覆盖 Top 20 常见问题(退款/价格/订单查询/医院导航) + +阶段 2(4周):意图识别 + 工单自动生成 + → 退款意图 → 自动创建工单 + 推送客服审核 + → 投诉意图 → 自动升级 + 通知运营经理 + +阶段 3(持续):多模态客服 + → 用户上传医院票据照片 → AI 识别 → 自动填充订单信息 + → 语音输入 → AI 语音转文字 → 意图识别 +``` + +### 2.2 智能调度 — 从人工指派到 AI 最优匹配 + +**当前痛点**:调度员手动查看空闲陪诊师,凭经验指派,效率低且匹配质量不稳定。 + +**AI 赋能后**: + +| 能力 | 说明 | +|------|------| +| **智能匹配评分** | 订单需求(科室、语言、性别、距离)→ AI 对区域内空闲陪诊师打分排序,考虑:专业匹配度(儿科/妇产科/老年科)、历史好评率、距离医院远近、当前负载 | +| **动态定价建议** | 高峰期/恶劣天气/偏远地区 → AI 建议调价系数,平衡供需 | +| **路线规划** | 多订单组合 → AI 规划陪诊师最优服务路线,减少通勤时间 | +| **异常预警** | 陪诊师超时未到达 → AI 检测异常 → 自动通知备选陪诊师 | +| **排班预测** | 基于历史数据预测各时段订单量 → 提前建议陪诊师排班 | + +**匹配模型示例**: + +``` +匹配得分 = w1×专业匹配(0~1) + w2×好评率(0~1) + w3×距离归一化(0~1) + w4×负载系数 + +AI 输出: + 陪诊师A: 得分 0.92 ← 推荐(妇产科专业,距离200m,好评98%) + 陪诊师B: 得分 0.78 + 陪诊师C: 得分 0.65 + +调度员确认 → 一键派单,或手动调整 +``` + +### 2.3 智能审核 — 认证/内容/退款自动化 + +| 场景 | 当前 | AI 赋能后 | +|------|------|---------| +| **陪诊师实名认证** | 人工查看身份证照片、资质证书 | AI OCR 识别+公安部接口校验+自动标记异常(PS痕迹、过期证件) | +| **服务评价审核** | 无审核机制 | AI 检测刷评(相似文本、异常频率)→ 自动标记疑似刷单评价 | +| **退款审核** | 客服逐单审核 | AI 预审:规则内退款(如服务未开始)自动通过;异常退款(频繁退款用户)自动标记人工复核 | +| **推广反作弊** | 无 | AI 检测刷量(同设备多账号、异常邀请转化率)→ 自动风控 | +| **护理资讯审核** | 无 | AI 审核文章内容合规性(医疗广告法、虚假宣传检测) | + +### 2.4 智能推广 — AI 生成 + 个性化推荐 + +| 能力 | 说明 | +|------|------| +| **AI 生成推广文案** | 推广员输入关键词("陪诊""老年人""北京")→ AI 生成 3 条朋友圈文案 + 配图建议 | +| **AI 生成分享海报** | 自动从模板库中选择 + 替换推广员头像/邀请码 + 生成个性化文案 | +| **智能推荐目标人群** | 推广员分享到朋友圈 → AI 分析谁更可能点击/转化,建议推广员定向邀请 | +| **个性化推广策略** | 根据推广员的社交圈特征,AI 建议最佳推广话术和时间 | + +### 2.5 智能运营 — 数据洞察 + 预测决策 + +| 能力 | 说明 | +|------|------| +| **自然语言查询数据** | 运营经理:"上个月北京的订单量是多少?""哪个陪诊师接单最多?"→ AI 转 SQL 查询 → 返回表格+图表 | +| **异常检测** | AI 实时监控:退款率突增、某区域订单暴跌、某陪诊师差评集中 → 自动预警推送 | +| **服务评价情感分析** | 患者评价文本 → AI 分析情感倾向 + 提取关键词("态度好""迟到""不专业")→ 自动分类归档 | +| **竞品监控** | AI 定期抓取竞品信息(小程序/公众号/美团)→ 生成简报:价格变化、新服务上线、用户评价趋势 | +| **月度运营报告** | 一键生成:本月订单趋势、收入分析、角色绩效、问题汇总、改进建议 | + +### 2.6 智能安全 — 让平台从"有漏洞"到"可靠" + +| 能力 | 说明 | +|------|------| +| **AI 代码审计** | 定期扫描后端代码,标记:密钥硬编码、SQL 注入风险、权限缺失 | +| **异常行为检测** | 登录行为分析:异地登录、短时间大量请求、异常金额提现 → 自动触发风控 | +| **内容合规检测** | 用户提交的评价/昵称/图片 → AI 检测涉黄涉政涉暴内容 → 自动拦截 | +| **隐私数据检测** | AI 扫描日志输出,自动发现并告警:手机号明文、身份证号、银行卡号泄露 | + +--- + +## 三、技术架构:AI 如何集成 + +### 3.1 架构总览 + +``` +┌──────────────────────────────────────────────────┐ +│ 小程序 / App / Web │ +└──────────────────────┬───────────────────────────┘ + │ +┌──────────────────────▼───────────────────────────┐ +│ API Gateway (Spring Boot) │ +│ /api/v1/ai/chat /api/v1/ai/match /api/v1/ai/* │ +└──────────────────────┬───────────────────────────┘ + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ +┌───────────┐ ┌───────────┐ ┌───────────┐ +│ AI Service │ │ AI Audit │ │ AI Ops │ +│ 对话/匹配 │ │ 审核/风控 │ │ 分析/报表 │ +└─────┬─────┘ └─────┬─────┘ └─────┬─────┘ + │ │ │ + └──────────────┼──────────────┘ + ▼ +┌──────────────────────────────────────┐ +│ LLM Proxy (通用代理层) │ +│ 限流 / 缓存 / fallback / 成本控制 │ +└──────────────────┬───────────────────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ +┌─────────┐ ┌─────────┐ ┌──────────┐ +│ DeepSeek │ │ 通义千问 │ │ 本地模型 │ +│ (主力) │ │ (备用) │ │ (敏感数据) │ +└─────────┘ └─────────┘ └──────────┘ +``` + +### 3.2 模型选择策略 + +| 场景 | 推荐模型 | 原因 | +|------|---------|------| +| 客服对话 | DeepSeek V3 / 通义千问 | 中文能力强,成本低 | +| 意图识别+分类 | DeepSeek V3 / Qwen3 | 结构化输出稳定 | +| 审核/风控(含隐私数据) | 本地部署 Qwen2.5-7B | 数据不出域,满足合规 | +| 代码审计 | Claude Opus 4.6 | 代码理解能力最强 | +| 数据分析/NL2SQL | DeepSeek V3 | 逻辑推理强 | +| 文本生成(推广文案) | DeepSeek / 通义千问 | 创意写作能力好 | + +### 3.3 关键设计原则 + +| 原则 | 说明 | +|------|------| +| **AI 辅助,不替代** | 所有 AI 决策都可追溯、可覆盖。退款审核 AI 预审但人工确认;匹配建议 AI 打分但调度员拍板 | +| **敏感数据不出域** | 涉及用户隐私(手机号、身份证、医疗信息)的流程走本地模型或脱敏后再调用云端 API | +| **全链路可观测** | 每次 AI 调用的输入/输出/耗时/成本 全记录,支持审计回溯 | +| **灰度上线** | 每个 AI 能力先覆盖 20% 流量 → 验证效果 → 逐步扩大 | +| **成本可控** | LLM Proxy 层统一缓存相同问题、限流、降级(高峰期回退到规则引擎) | + +--- + +## 四、实施优先级与 ROI + +``` +优先级矩阵: + + 高影响 + │ + 智能客服(2周) │ 智能调度(4周) + 智能审核(3周) │ 智能安全(持续) + │ + ───────────────────┼──────────────────── + │ + 智能推广(4周) │ 智能运营(6周) + │ + 低影响 + + 横轴:实现难度(左=易,右=难) +``` + +### 推荐落地顺序 + +| 阶段 | 时间 | AI 能力 | 投入 | 预期效果 | +|------|------|---------|------|---------| +| **Phase 1** | 第 1~2 周 | 智能客服(FAQ 问答 + 订单查询) | 1 人 | 客服工作量减少 60% | +| **Phase 2** | 第 3~5 周 | 智能审核(认证 OCR + 内容审核) | 1 人 | 审核效率 5×,错误率降低 | +| **Phase 3** | 第 6~9 周 | 智能调度(匹配打分 + 异常预警) | 1.5 人 | 匹配效率 3×,投诉率降低 | +| **Phase 4** | 第 10~14 周 | 智能运营(数据问答 + 舆情监控) | 1.5 人 | 决策速度 5× | +| **Phase 5** | 持续 | 智能安全(代码审计 + 行为风控) | 1 人 | 安全事件减少 80%+ | + +**总投入**:1 人(AI 工程师)全职 3~4 个月,或兼职 6 个月。 +**AI API 月度成本**:约 3,000~10,000 元(DeepSeek 等国产模型价格极低)。 + +--- + +## 五、成本估算 + +| 项目 | 月度成本 | 说明 | +|------|---------|------| +| DeepSeek API (客服+调度+审核) | 2,000~5,000 元 | 国产模型 1 元/百万 token | +| 本地模型服务器 (Qwen2.5-7B) | 500~1,000 元 | 腾讯云 GPU 实例或共享 | +| LLM Proxy 缓存 | 节省 30~50% API 费用 | 相同问题不重复调用 | +| **月度总计** | **~3,000~8,000 元** | 远低于一个人工客服工资 | + +对比:一个客服专员月薪 5,000~8,000 元 + 社保。AI 客服 24 小时在线,成本仅为其 1/10。 + +--- + +## 六、风险与边界 + +| 风险 | 应对 | +|------|------| +| **AI 幻觉** | 客服回答强制引用知识库来源,不确定时说"建议联系人工客服"而非瞎编 | +| **医疗建议风险** | AI 严禁给出医疗建议,检测到医疗问题时统一回复"请咨询医生" | +| **数据合规** | 医疗健康数据不传云端 AI,仅本地模型处理;对话数据加密存储 | +| **过度依赖** | 所有 AI 决策保留人工确认环节,关键操作(退款、结算)AI 只做预审 | +| **用户抵触** | 客服场景始终提供"转人工"入口,AI 是加速器不是替代品 | + +--- + +## 七、演进路线图(汇总) + +``` +2026 Q3-Q4 2027 Q1 2027 Q2-Q3 2027 Q4+ +───────── ───────── ─────────── ───────── +多角色系统上线 AI 客服上线 AI 调度+审核 AI 运营+安全 + +安全整改 FAQ 自动应答 智能匹配评分 自然语言查数据 +数据库改造 订单自助查询 认证 OCR 审核 异常行为风控 +后端服务 意图识别分流 内容合规检测 AI 代码审计 +管理后台 知识库 RAG AI 生成推广文案 竞品监控 +小程序多角色 月度自动报告 +Android 多角色 + +┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ +│ 平台化 │ → │ 智能化 │ → │ 自动化 │ → │ 预测化 │ +│ (多角色) │ │ (AI 辅助) │ │ (AI 决策) │ │ (AI 预判) │ +│ 竞争力 4/5 │ │ 竞争力 4.5/5│ │ 竞争力 4.8/5│ │ 竞争力 5/5 │ +└────────────┘ └────────────┘ └────────────┘ └────────────┘ +``` + +--- + +## 八、终局思考:陪诊平台的 AI Native 形态 + +当 AI 渗透到每个角色后,平台的终极形态是什么? + +``` +患者的日常: + "帮我找一个明天上午去协和医院心内科的陪诊师" + → AI 理解意图 → 自动匹配 → 生成订单 → 推荐最优陪诊师 + → 患者确认 → 支付 → AI 推送行程提醒 → 服务后 AI 回访评价 + +陪诊师的日常: + AI 助手说:"明天有 3 个订单匹配到你:协和心内科(9:00)、天坛神内(14:00)、社区取药(16:30)" + → 路线已规划,预计通勤 35 分钟 + → 协和的王阿姨是第三次服务,她偏好普通话好、有耐心的陪诊师(所以你被优先匹配) + +运营经理的日常: + 打开看板 → AI 已经生成了今早的简报: + "昨日订单 128 单(+12%),退款 3 单(正常范围) + 异常预警:朝阳区 3 位陪诊师连续 3 天未接单,建议跟进 + 机会提示:海淀区周末订单量上涨 30%,建议增加该区域推广投入" + +客服专员的日常: + AI 已处理了 80% 的咨询 + 你只需要处理 5 个需要人工判断的工单: + - 工单 #3421:退款争议(AI 已整理双方陈述和订单详情,你只需判断) + - 工单 #3427:服务投诉(AI 已标记为高风险,患者情绪激烈) +``` + +**终局竞争力:5/5** + +从"工具"→"平台"→"智能平台"→"预测型平台",每一步都是壁垒的叠加: + +| 阶段 | 壁垒 | 可复制性 | +|------|------|:--:| +| 1. 功能可用 | 代码 | 容易 | +| 2. 多角色生态 | 利益网络 | 难 | +| 3. AI 智能 | 数据+模型+业务理解 | 很难 | +| 4. 预测决策 | 数据飞轮(越多数据→越准→越多用户→越多数据) | **几乎不可复制** | + +--- + +> **核心观点**:AI 不是锦上添花,而是"智能化"这个维度从 0 到 1 的补齐。多角色系统解决了"谁来做",AI 解决的是"做得更快、更准、更安全"。两者叠加,平台从能用变成好用,从好用变成不可替代。 diff --git a/docs/DDL迁移脚本.sql b/docs/DDL迁移脚本.sql new file mode 100644 index 0000000..cbeb564 --- /dev/null +++ b/docs/DDL迁移脚本.sql @@ -0,0 +1,166 @@ +-- ============================================ +-- 瑞来健康 多角色系统 DDL 迁移脚本 +-- 版本: v2.0 +-- 日期: 2026-06-08 +-- 执行方式: 在 rlz 生产库依次执行以下语句 +-- ============================================ + +-- ============================================ +-- 第一部分: 新建表 (4张) +-- ============================================ + +-- 1. 推广关系表 +DROP TABLE IF EXISTS `rlz_promotion_relation`; +CREATE TABLE `rlz_promotion_relation` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `promoter_id` bigint(20) NOT NULL COMMENT '推广员用户ID', + `invited_user_id` bigint(20) NOT NULL COMMENT '被邀请用户ID', + `level` tinyint(4) NOT NULL COMMENT '推广层级: 1=一级 2=二级 3=三级', + `parent_promoter_id` bigint(20) DEFAULT NULL COMMENT '上级推广员ID(二级/三级时指向直接上级)', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '绑定时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_invited` (`invited_user_id`), + KEY `idx_promoter` (`promoter_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广关系表'; + +-- 2. 分润记录表 +DROP TABLE IF EXISTS `rlz_profit_record`; +CREATE TABLE `rlz_profit_record` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `order_id` bigint(20) NOT NULL COMMENT '关联订单ID', + `user_id` bigint(20) NOT NULL COMMENT '受益人用户ID', + `role_type` varchar(10) NOT NULL COMMENT '角色类型: caregiver/dispatcher/promoter/operations', + `profit_type` varchar(10) NOT NULL COMMENT '分润类型: service/dispatch/promotion/regional', + `amount` decimal(10,2) NOT NULL COMMENT '分润金额', + `rate` decimal(5,2) DEFAULT NULL COMMENT '分润比例(%)', + `status` char(1) DEFAULT '0' COMMENT '0=待结算 1=已结算', + `settle_time` datetime DEFAULT NULL COMMENT '结算时间', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_order` (`order_id`), + KEY `idx_user` (`user_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分润记录表'; + +-- 3. 推广佣金配置表 +DROP TABLE IF EXISTS `rlz_promoter_config`; +CREATE TABLE `rlz_promoter_config` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `config_key` varchar(50) NOT NULL COMMENT '配置键', + `config_value` varchar(50) NOT NULL COMMENT '配置值', + `description` varchar(200) DEFAULT NULL COMMENT '说明', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `config_key` (`config_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广佣金配置表'; + +-- 默认配置数据 +INSERT INTO `rlz_promoter_config` (`config_key`, `config_value`, `description`) VALUES +('level1_rate', '5', '一级推广佣金比例(%)'), +('level2_rate', '3', '二级推广佣金比例(%)'), +('level3_rate', '2', '三级推广佣金比例(%)'), +('caregiver_rate', '80', '陪诊师服务费比例(%)'), +('dispatcher_rate', '2', '调度员分润比例(%)'), +('operations_rate', '1', '运营经理区域分润比例(%)'); + +-- 4. 运营区域表 +DROP TABLE IF EXISTS `rlz_region`; +CREATE TABLE `rlz_region` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL COMMENT '区域名称', + `parent_id` bigint(20) DEFAULT '0' COMMENT '上级区域ID(0=根)', + `level` tinyint(4) DEFAULT NULL COMMENT '层级: 1=省 2=市 3=区', + `manager_id` bigint(20) DEFAULT NULL COMMENT '运营经理用户ID', + `status` char(1) DEFAULT '0' COMMENT '0=正常 1=停用', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运营区域表'; + +-- ============================================ +-- 第二部分: 现有表扩展 (4张) +-- ============================================ + +-- 1. 系统用户表扩展 +ALTER TABLE `sys_user` + ADD COLUMN IF NOT EXISTS `user_type` varchar(10) DEFAULT '00' COMMENT '用户类型: 00=患者 01=陪诊师 03=运营经理 04=客服 05=调度 06=推广员 0B=机构 0C=管理员', + ADD COLUMN IF NOT EXISTS `promotion_code` varchar(20) DEFAULT NULL COMMENT '邀请码', + ADD COLUMN IF NOT EXISTS `parent_promoter_id` bigint(20) DEFAULT NULL COMMENT '上级推广员ID', + ADD COLUMN IF NOT EXISTS `region_id` bigint(20) DEFAULT NULL COMMENT '所属区域ID'; + +-- (如果数据库版本不支持 IF NOT EXISTS, 请单独执行以下语句): +-- ALTER TABLE `sys_user` ADD COLUMN `user_type` varchar(10) DEFAULT '00' COMMENT '用户类型'; +-- ALTER TABLE `sys_user` ADD COLUMN `promotion_code` varchar(20) DEFAULT NULL COMMENT '邀请码'; +-- ALTER TABLE `sys_user` ADD COLUMN `parent_promoter_id` bigint(20) DEFAULT NULL COMMENT '上级推广员ID'; +-- ALTER TABLE `sys_user` ADD COLUMN `region_id` bigint(20) DEFAULT NULL COMMENT '所属区域ID'; + +-- 2. 订单表扩展 +ALTER TABLE `rlz_order` + ADD COLUMN IF NOT EXISTS `promoter_id` bigint(20) DEFAULT NULL COMMENT '推广员ID(谁推广的此单)', + ADD COLUMN IF NOT EXISTS `dispatch_type` varchar(10) DEFAULT 'auto' COMMENT '派单方式: auto=自动 manual=手动', + ADD COLUMN IF NOT EXISTS `dispatcher_id` bigint(20) DEFAULT NULL COMMENT '调度员ID(手动派单时记录)'; + +-- 3. 系统角色新增 (如果已存在请跳过) +INSERT IGNORE INTO `sys_role` (`role_id`, `role_name`, `role_key`, `role_sort`, `status`, `create_time`) VALUES +(103, '运营经理', 'operations', 1, '0', NOW()), +(104, '客服专员', 'service', 2, '0', NOW()), +(105, '调度员', 'dispatcher', 2, '0', NOW()), +(106, '推广员', 'promoter', 3, '0', NOW()); + +-- 4. 系统菜单新增 (如果已存在请跳过) +-- 一级菜单: 财务管理 +INSERT IGNORE INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_time`) VALUES +(3000, '财务管理', 0, 9, 'finance', NULL, 'M', '0', '0', NULL, 'money', NOW()); + +-- 二级菜单: 财务子模块 +INSERT IGNORE INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_time`) VALUES +(3001, '退款审核', 3000, 1, 'refund', 'finance/refund/index', 'C', '0', '0', 'system:order:list', 'build', NOW()), +(3010, '结算管理', 3000, 2, 'settlement', 'finance/settlement/index', 'C', '0', '0', 'system:order:list', 'build', NOW()), +(3040, '平台配置', 3000, 3, 'platformConfig', 'finance/platformConfig/index', 'C', '0', '0', 'system:platformConfig:list', 'build', NOW()), +(3054, '分润记录', 2010, 1, 'profit', 'finance/profit/index', 'C', '0', '0', 'system:profit:list', 'build', NOW()); + +-- 二级菜单: 调度管理 +INSERT IGNORE INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_time`) VALUES +(3052, '调度管理', 3000, 4, 'dispatch', 'system/dispatch/index', 'C', '0', '0', 'system:dispatch:list', 'build', NOW()); + +-- 一级菜单下的新模块 +INSERT IGNORE INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_time`) VALUES +(3042, '推广管理', 1, 6, 'promotion', 'system/promotion/index', 'C', '0', '0', 'system:promotion:list', 'peoples', NOW()), +(3047, '区域管理', 1, 7, 'region', 'system/region/index', 'C', '0', '0', 'system:region:list', 'tree', NOW()); + +-- ============================================ +-- 第三部分: 权限分配 (sys_role_menu) +-- 将新菜单分配给对应角色 +-- ============================================ + +-- 运营经理(103) 权限 +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES +(103, 3000), -- 财务管理 +(103, 3001), -- 退款审核 +(103, 3010), -- 结算管理 +(103, 3054), -- 分润记录 +(103, 3047); -- 区域管理 + +-- 客服专员(104) 权限 +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES +(104, 3000), -- 财务管理 +(104, 3001), -- 退款审核 +(104, 3054); -- 分润记录 + +-- 调度员(105) 权限 +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES +(105, 3000), -- 财务管理 +(105, 3052), -- 调度管理 +(105, 3054); -- 分润记录 + +-- 推广员(106) 权限 +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES +(106, 3000), -- 财务管理 +(106, 3042), -- 推广管理 +(106, 3054); -- 分润记录 + +-- ============================================ +-- 验证脚本 +-- ============================================ +-- SELECT TABLE_NAME, TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_SCHEMA='rlz' AND TABLE_NAME LIKE 'rlz_%'; +-- SELECT role_id, role_name FROM sys_role WHERE role_id >= 100; +-- SELECT user_id, user_name, user_type FROM sys_user; diff --git a/docs/多角色系统与分润方案.md b/docs/多角色系统与分润方案.md new file mode 100644 index 0000000..cd9ee28 --- /dev/null +++ b/docs/多角色系统与分润方案.md @@ -0,0 +1,715 @@ +# 瑞来健康 — 多角色系统与分润方案 + +> 撰写日期:2026-06-07 +> 基于现有四端架构(Spring Boot 后端 / Vue2 管理后台 / Android 陪护端 / 微信小程序患者端) +> 回答核心问题:是否应该在现有 B-C 两角色之外引入多角色体系?如果可行,怎么做? + +--- + +## 一、可行性判断 + +### 1.1 结论:可行,且商业价值高 + +| 维度 | 评估 | 说明 | +|------|------|------| +| 技术可行性 | **高** | RuoYi 框架自带 RBAC(角色-菜单-权限),sys_role / sys_user_role 表已就绪,userType 字段已区分 B/C/00 | +| 业务可行性 | **高** | 陪诊 O2O 天然涉及多方协作——接单、派单、客服、推广都需要不同的人 | +| 市场价值 | **高** | 多角色分润是平台规模化运营的前提——一个人做所有事无法复制,角色分工才能扩张 | +| 风险可控性 | **中** | 分润计算逻辑复杂,需严谨的财务对账机制,但属于工程问题,非根本障碍 | + +### 1.2 为什么现在做合适 + +当前系统只有两角色(患者C / 陪护B / 管理员00),这导致了几个瓶颈: + +1. **陪护员既要接单又要拉客** — 没有推广角色,平台流量全靠自然增长 +2. **管理员要处理所有纠纷和派单** — 没有客服和调度角色,管理员瓶颈明显 +3. **没有激励层** — 陪护员拿固定服务费,没有推广动力;推广人员没有变现路径 +4. **无法规模化** — 每个城市都需要一套运营班子,但当前架构不支持 + +多角色分润体系是平台从"工具"升级为"平台"的核心一步。 + +--- + +## 二、角色体系设计 + +### 2.1 七角色全景 + +``` + ┌─────────────┐ + │ 超级管理员 │ (sys admin, userType=00) + │ platform │ + └──────┬──────┘ + │ + ┌───────────────────────┼───────────────────────┐ + │ │ │ + ┌────▼─────┐ ┌──────▼──────┐ ┌─────▼────┐ + │ 运营经理 │ │ 财务主管 │ │ 城市合伙人 │ + │operations │ │ finance │ │ partner │ + └────┬─────┘ └──────┬──────┘ └─────┬────┘ + │ │ │ + ┌────▼─────┐ ┌──────▼──────┐ ┌─────▼────┐ + │ 客服专员 │ │ 调度员 │ │ 推广员 │ + │ service │ │dispatcher │ │ promoter │ + └──────────┘ └─────────────┘ └─────┬────┘ + │ + ┌───────────────────────────────┘ + │ + ┌─────▼─────┐ + │ 陪诊师 │ (existing, userType=B) + │ caregiver │ + └───────────┘ +``` + +### 2.2 角色详细定义 + +#### 角色 1:超级管理员 (platform_admin) — 已有 + +| 属性 | 说明 | +|------|------| +| userType | `00` | +| 职责 | 系统配置、角色分配、财务审批、全局数据查看 | +| 分润 | 无(拿固定工资或平台利润分红,不走系统分润) | +| 终端 | Web 管理后台 | +| 权限范围 | 全部菜单、全部数据 | + +#### 角色 2:运营经理 (operations_manager) + +| 属性 | 说明 | +|------|------| +| userType | `03` | +| 职责 | 管理某个城市/区域的运营,统管调度员和客服,查看区域数据报表 | +| 分润 | **区域流水的 1%~3%**(按区域全部订单金额计提) | +| 终端 | Web 管理后台(受限菜单) + Android App + 小程序 | +| 权限范围 | 所属区域的订单、用户、医院数据 | + +#### 角色 3:客服专员 (service_agent) + +| 属性 | 说明 | +|------|------| +| userType | `04` | +| 职责 | 处理用户投诉、退款审核、纠纷调解、订单异常处理 | +| 分润 | **固定工资 + 处理工单提成**(每处理一单 N 元,或按月考核) | +| 终端 | Web 管理后台(订单/退款模块) + Android App + 小程序 | +| 权限范围 | 订单管理(查看+退款审核)、用户管理(查看) | + +#### 角色 4:调度员 (dispatcher) + +| 属性 | 说明 | +|------|------| +| userType | `05` | +| 职责 | 手动分配订单给陪诊师(当系统自动派单失败时)、优化陪诊师排班、处理紧急调度 | +| 分润 | **每成功调度一单抽取 1%~2%**(从订单金额中计提) | +| 终端 | Web 管理后台(订单+医院+用户) + Android App + 小程序 | +| 权限范围 | 订单管理(指派/改派)、陪护人员管理(区域范围内)、医院管理 | + +#### 角色 5:推广员 / 流量合伙人 (promoter) + +| 属性 | 说明 | +|------|------| +| userType | `06` | +| 职责 | 通过分享链接/二维码/社交媒体引流新用户,推广平台服务 | +| 分润 | **三级分润(见 4.3 节)**— 被推广用户消费金额的 5%~15% | +| 终端 | 小程序推广端(独立 Tab 或页面) | +| 权限范围 | 查看推广数据(邀请人数、订单数、佣金)、提现 | + +**这是平台规模化增长的核心角色。** 推广员不参与服务交付,只负责流量获取,通过佣金激励实现病毒式增长。 + +#### 角色 6:陪诊师 (caregiver) — 已有 + +| 属性 | 说明 | +|------|------| +| userType | `B` | +| 职责 | 接单、执行陪诊服务、完成订单 | +| 分润 | **服务费的 80%**(平台抽 20%,详见《陪诊员服务费分账方案》) | +| 终端 | Android App(完整) + 小程序(轻量) | +| 权限范围 | 查看自己订单、接单/拒单/开始/完成服务、查看收入、提现 | +| 说明 | 小程序提供轻量入口(接单/完成/收入查看),降低陪诊师使用门槛;Android App 提供完整体验(实时推送、后台服务、支付回调) | + +#### 角色 7:患者/客户 (customer) — 已有 + +| 属性 | 说明 | +|------|------| +| userType | `C` | +| 职责 | 下单、支付、评价 | +| 分润 | 无(消费者) | +| 终端 | 微信小程序 | +| 权限范围 | 下单、查看自己订单、申请退款、收藏、评价 | + +--- + +## 三、角色选择流程 + +### 3.1 注册时选择角色 + +``` +小程序注册流程(改造后): + +授权微信登录 + → 获取微信昵称/头像 + → 填写手机号 + 验证码 + → 【新增】选择身份(三选一): + ○ 我需要陪诊服务 → userType = C(患者) + ○ 我提供陪诊服务 → userType = B(陪诊师,需实名认证) + ○ 我推广赚佣金 → userType = 06(推广员) + → 注册完成,进入对应首页 + +> **注意**:调度员(05)、客服(04)、运营经理(03) 不在注册页开放自选,需由超级管理员在后台分配。 +> 注册页只开放三种 C 端身份,保证入口简洁。 + +管理后台创建用户时(已有用户管理页面改造): + → 点击"新增用户" + → 填写基本信息 + → 【新增】选择角色:管理员/运营经理/客服/调度员/推广员/陪诊师/患者 + → 系统自动分配对应权限 +``` + +### 3.2 角色切换 + +- 同一用户可以拥有**多个角色**(如陪诊师也可以同时是推广员) +- 小程序底部 Tab 根据角色组合动态展示 +- 角色切换入口:我的 → 身份管理 → 切换到 XX 身份 + +### 3.3 各角色终端矩阵 + +| 角色 | 管理后台(Web) | 小程序 | Android App | +|------|:--:|:--:|:--:| +| 超级管理员 | ✅ 全部 | ❌ | ❌ | +| 运营经理 | ✅ 受限 | ✅ | ✅ | +| 客服专员 | ✅ 受限 | ✅ | ✅ | +| 调度员 | ✅ 受限 | ✅ | ✅ | +| 推广员 | ❌ | ✅ 核心 | ❌ | +| 陪诊师 | ❌ | ✅ 轻量 | ✅ 完整 | +| 患者 | ❌ | ✅ | ❌ | + +### 3.4 终端架构决策 + +#### 微信小程序:同一个小程序,自定义 TabBar + 角色驱动 + +**不新建独立小程序。** 所有非管理员角色共用同一个小程序代码库,通过微信[自定义 TabBar](https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html) 根据 `userType` 动态切换底部导航和首页内容。 + +**理由:** + +| | 新建小程序 | 同一小程序扩展 | +|------|----------|--------------| +| 注册审核 | 每个独立审核,医疗健康类目门槛高 | 无需额外审核 | +| 认证费用 | 300元/年 × N个 | 0 | +| 代码维护 | N 套或 monorepo | 1 套,utils/ 共享 | +| 用户切换 | 小程序间跳转,体验割裂 | 同一应用内切换身份 | +| 推广转化 | 患者→推广员需跳另一个小程序 | 自然转化,无跳出 | + +**各角色看到的底部 Tab:** + +``` +患者(C) 陪诊师(B) — 轻量入口 推广员(06) +┌────────────────┐ ┌────────────────┐ ┌────────────────┐ +│ 🏠 首页 │ │ 📋 待接单 │ │ 📊 推广数据 │ +│ 🛒 服务 │ │ 🔄 进行中 │ │ 👥 我的团队 │ +│ 📋 订单 │ │ 📦 已完成 │ │ 💰 我的佣金 │ +│ 👤 我的 │ │ 👤 我的 │ │ 👤 我的 │ +└────────────────┘ └────────────────┘ └────────────────┘ + +调度/客服/运营(03/04/05) +┌────────────────┐ +│ 📋 待处理 │ +│ 📦 全部订单 │ +│ 💰 收益明细 │ +│ 👤 我的 │ +└────────────────┘ +``` + +**文件结构:** + +``` +coupon/ +├── pages/ ← 患者端页面(现有,不动) +├── pages-promoter/ ← 推广端子包(新增) +│ ├── dashboard/ ← 推广数据看板 +│ ├── team/ ← 我的团队 +│ ├── commission/ ← 佣金明细 + 提现 +│ └── share/ ← 分享素材/海报 +├── pages-work/ ← 工作台子包(新增,调度+客服+运营共用) +│ ├── dispatch/ ← 调度:待接单 + 指派陪诊师 +│ ├── refund/ ← 客服:退款审核列表 +│ ├── dashboard/ ← 运营:区域数据看板 +│ └── order-manage/ ← 通用:订单搜索/查看 +├── custom-tab-bar/ ← 自定义 TabBar(按角色渲染) +└── app.json ← 注册所有页面 + 子包 +``` + +#### Android App:陪诊师→多角色共用 + +现有 Android App (`peizhen`) 由一个三 Tab 陪护端(首页/客户/我的)扩展为支持陪诊师/调度/客服/运营四种角色。登录后根据后端返回的 `userType` 动态加载不同 Fragment 组合。 + +**推广员不适合 Android App** — 其核心动作是微信生态内的分享裂变,App 下载门槛反成障碍。 + +**改造点:** + +| 改动 | 说明 | +|------|------| +| `MainActivity` 动态 Tab | 当前写死 3 个固定 Tab(首页/客户/我的),改为根据 userType 组装 | +| 角色判断入口 | `SplashActivity` → login → 后端返回 userType → 决定加载哪些 Fragment | +| 调度员界面 | 待接单列表 + "指派"按钮 + 选择陪诊师弹窗 + 改派(复用现有订单列表 UI) | +| 客服界面 | 退款审核 Tab + 通过/驳回 + 原因填写(复用现有订单详情 UI) | +| 运营看板 | 可先用 WebView 加载后台运营页面,快速上线 | + +**各角色在 App 中看到的 Tab:** + +``` +陪诊师(B) — 现有 调度员(05) 客服(04) 运营(03) +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ 🏠 首页(接单) │ │ 📋 待派单 │ │ 📋 退款审核 │ │ 📊 运营看板 │ +│ 👥 客户(订单) │ │ 📦 全部订单 │ │ 📦 全部订单 │ │ 📦 全部订单 │ +│ 👤 我的 │ │ 👤 我的 │ │ 👤 我的 │ │ 👤 我的 │ +└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ +``` + +> **陪诊师说明**:Android App 是陪诊师的完整工具(实时推送、后台服务、支付回调),但小程序也提供陪诊师轻量入口(接单/完成/收入查看),降低新陪诊师的注册门槛——无需下载 App 即可开始接单。 + +--- + +## 四、分润模型设计 + +### 4.1 分润总体结构 + +``` + 订单金额(患者支付 100%) + │ + ┌──────────────┼──────────────┐ + │ │ │ + 平台抽成 服务费 推广佣金 + (20%) (待分配) (5%~15%) + │ │ │ + 平台收入 陪诊师 80% 推广员佣金 + 调度员 1~2% (从平台抽成中出) + 运营 1~3% (从平台抽成中出) +``` + +### 4.2 各角色分润规则 + +| 角色 | 分润来源 | 比例/金额 | 结算周期 | 结算条件 | +|------|---------|----------|---------|---------| +| **陪诊师** | 订单服务费 | 订单金额 × 80% | T+7 | 订单完成 + 无退款 | +| **调度员** | 订单服务费 | 订单金额 × 1%~2% | T+7 | 手动调度的订单 + 完成 | +| **运营经理** | 区域流水 | 区域月流水 × 1%~3% | 月结 | 区域达标 | +| **推广员** | 平台抽成 | 被邀请用户消费 × 5%~15% | T+7 | 订单完成 + 无退款 | +| **客服专员** | 固定+计件 | 底薪 + 处理工单 × N元 | 月结 | 工单关闭 | +| **超级管理员** | 平台利润 | 非系统分润 | — | — | + +### 4.3 推广员三级分润(核心增长引擎) + +``` +推广员A 直接邀请 → 用户X +用户X 下单 100元 → A 拿一级佣金(5%,即 5元) + +用户X 也成为推广员 → 邀请用户Y +用户Y 下单 100元 → X 拿一级佣金(5%,即 5元) + → A 拿二级佣金(3%,即 3元) + +用户Y 也成为推广员 → 邀请用户Z +用户Z 下单 100元 → Y 拿一级佣金(5%,即 5元) + → X 拿二级佣金(3%,即 3元) + → A 拿三级佣金(2%,即 2元) +``` + +**分润比例可配置:** + +| 级别 | 默认比例 | 配置键 | +|------|---------|--------| +| 一级 | 5% | `promoter_level1_rate` | +| 二级 | 3% | `promoter_level2_rate` | +| 三级 | 2% | `promoter_level3_rate` | + +**总计推广佣金上限**:订单金额的 10%(三级合计),从平台抽成的 20% 中支出。 + +### 4.4 分润示例 + +**场景**:一笔 500 元订单,推广员三级关系都存在,调度员手动分派: + +| 角色 | 金额 | 计算 | +|------|------|------| +| 订单金额 | 500.00 | 患者支付 | +| 平台抽成(20%) | 100.00 | 500 × 20% | +| **陪诊师所得** | 380.00 | 500 × 80% - 20(调度) | +| **调度员所得** | 10.00 | 500 × 2% | +| **推广员一级** | 25.00 | 500 × 5% | +| **推广员二级** | 15.00 | 500 × 3% | +| **推广员三级** | 10.00 | 500 × 2% | +| **平台净收入** | 60.00 | 100 - 25 - 15 - 10 | +| 运营经理月结 | — | 区域月度流水 × 1~3%(平台另行支出) | + +--- + +## 五、数据库改动 + +### 5.1 新增表 + +#### 用户角色关系表 `sys_user_role_ext`(已有 sys_user_role 可扩展) + +```sql +-- 扩展现有 sys_user 表,userType 支持新角色 +ALTER TABLE sys_user MODIFY COLUMN userType VARCHAR(10) COMMENT 'C=患者 B=陪护 00=超管 03=运营 04=客服 05=调度 06=推广员'; + +-- 推广关系表 +CREATE TABLE rlz_promotion_relation ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + promoter_id BIGINT NOT NULL COMMENT '推广员用户ID', + invited_user_id BIGINT NOT NULL COMMENT '被邀请用户ID', + level TINYINT NOT NULL COMMENT '关系层级: 1=一级 2=二级 3=三级', + parent_promoter_id BIGINT COMMENT '上级推广员ID(二级/三级时)', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_invited (invited_user_id), + KEY idx_promoter (promoter_id) +) COMMENT='推广关系表'; + +-- 分润记录表 +CREATE TABLE rlz_profit_record ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + order_id BIGINT NOT NULL COMMENT '关联订单ID', + user_id BIGINT NOT NULL COMMENT '分润受益人', + role_type VARCHAR(10) NOT NULL COMMENT '角色: B=陪诊师 05=调度 06=推广 03=运营', + profit_type VARCHAR(10) NOT NULL COMMENT '分润类型: service=服务费 dispatch=调度 commission=推广 salary=底薪', + amount DECIMAL(10,2) NOT NULL COMMENT '分润金额', + rate DECIMAL(5,2) COMMENT '分润比例(%)', + status CHAR(1) DEFAULT '0' COMMENT '0=待结算 1=已结算 2=已打款', + settle_time DATETIME COMMENT '结算时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + KEY idx_order (order_id), + KEY idx_user (user_id), + KEY idx_status (status) +) COMMENT='分润记录表'; + +-- 推广员佣金配置表 +CREATE TABLE rlz_promoter_config ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + config_key VARCHAR(50) NOT NULL UNIQUE, + config_value VARCHAR(50) NOT NULL, + description VARCHAR(200), + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) COMMENT='推广员佣金配置'; + +INSERT INTO rlz_promoter_config (config_key, config_value, description) VALUES +('promoter_level1_rate', '5', '一级推广佣金比例(%)'), +('promoter_level2_rate', '3', '二级推广佣金比例(%)'), +('promoter_level3_rate', '2', '三级推广佣金比例(%)'), +('promoter_max_total_rate', '10', '推广佣金总上限(%)'), +('dispatch_rate', '2', '调度员分润比例(%)'), +('operations_rate', '2', '运营经理区域流水比例(%)'); + +-- 推广员提现记录表(可复用现有钱包逻辑) +-- 陪诊师已有 yue/leijijine 字段,推广员也可使用同字段 +``` + +### 5.2 现有表修改 + +| 表 | 改动 | +|----|------| +| `sys_user` | userType 枚举扩展,推广员需加 `promotion_code`(邀请码)、`parent_promoter_id`(上级推广员) | +| `rlz_order` | 增加 `promoter_id`(推广员ID)、`dispatch_type`(派单方式: auto/manual)、`dispatcher_id`(调度员ID) | +| `sys_menu` | 新增推广端、调度端、客服端菜单 | +| `sys_role` | 新增运营经理、客服、调度、推广员角色 | + +### 5.3 sys_user 字段扩展 + +```sql +ALTER TABLE sys_user + ADD COLUMN promotion_code VARCHAR(20) COMMENT '推广邀请码', + ADD COLUMN parent_promoter_id BIGINT COMMENT '上级推广员ID', + ADD COLUMN region_id BIGINT COMMENT '所属区域ID', + ADD UNIQUE KEY uk_promotion_code (promotion_code); + +-- 区域表 +CREATE TABLE rlz_region ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '区域名称', + parent_id BIGINT DEFAULT 0 COMMENT '上级区域ID', + level TINYINT COMMENT '1=省 2=市 3=区', + manager_id BIGINT COMMENT '运营经理ID', + status CHAR(1) DEFAULT '0', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP +) COMMENT='运营区域表'; +``` + +--- + +## 六、后端实现要点 + +### 6.1 新增核心 Service + +| Service | 职责 | +|---------|------| +| `RlzPromotionService` | 推广关系绑定、邀请码生成、佣金计算 | +| `RlzProfitService` | 分润计算引擎(订单完成时触发)、分润记录生成 | +| `RlzDispatchService` | 调度派单逻辑、手动/自动派单切换 | +| `RlzRegionService` | 区域管理、运营经理绑定 | + +### 6.2 分润计算引擎(核心) + +```java +// 订单支付成功后触发 +// 在 WxPayNotify 或 weixinPayOrderNext() 中调用 + +@Service +public class RlzProfitService { + + // 支付回调成功后调用 + @Transactional + public void calculateProfit(RlzOrder order) { + BigDecimal totalAmount = new BigDecimal(order.getYuguMoney()); + + // 1. 陪诊师服务费(80% - 调度费 - 运营费) + BigDecimal caregiverRate = new BigDecimal("80"); + BigDecimal caregiverAmount = totalAmount.multiply(caregiverRate).divide(new BigDecimal("100")); + + // 2. 调度员分润(如果手动调度) + if ("manual".equals(order.getDispatchType()) && order.getDispatcherId() != null) { + BigDecimal dispatchRate = getConfig("dispatch_rate", "2"); + BigDecimal dispatchAmount = totalAmount.multiply(dispatchRate).divide(new BigDecimal("100")); + caregiverAmount = caregiverAmount.subtract(dispatchAmount); + createProfitRecord(order.getId(), order.getDispatcherId(), "05", "dispatch", dispatchAmount, dispatchRate); + } + + // 3. 推广佣金(三级分润) + Long promoterId = order.getPromoterId(); + calculatePromoterCommission(order, promoterId, totalAmount); + + // 4. 陪诊师分润 + createProfitRecord(order.getId(), order.getBId(), "B", "service", caregiverAmount, caregiverRate); + + // 5. 运营经理分润(月结,此处记录区域流水) + recordRegionalRevenue(order); + } + + private void calculatePromoterCommission(RlzOrder order, Long promoterId, BigDecimal amount) { + // 向上追溯三级推广关系 + Long currentPromoterId = promoterId; + for (int level = 1; level <= 3; level++) { + if (currentPromoterId == null) break; + String rateKey = "promoter_level" + level + "_rate"; + BigDecimal rate = getConfig(rateKey, level == 1 ? "5" : level == 2 ? "3" : "2"); + BigDecimal commission = amount.multiply(rate).divide(new BigDecimal("100")); + createProfitRecord(order.getId(), currentPromoterId, "06", "commission", commission, rate); + + // 找上级推广员 + SysUser promoter = userService.selectUserById(currentPromoterId); + currentPromoterId = promoter.getParentPromoterId(); + } + } +} +``` + +### 6.3 新增 Controller + +| Controller | 路由前缀 | 说明 | +|-----------|---------|------| +| `RlzPromotionController` | `/system/promotion` | 推广数据、邀请码、团队列表 | +| `RlzProfitController` | `/system/profit` | 分润记录查询、结算管理 | +| `RlzDispatchController` | `/system/dispatch` | 调度派单、改派 | +| `RlzRegionController` | `/system/region` | 区域管理 CRUD | + +--- + +## 七、各端改动清单 + +### 7.1 管理后台 (Vue2) — 新增/改造页面 + +| 页面 | 路由 | 说明 | 工作估量 | +|------|------|------|---------| +| 角色分配 | `/system/user/roleAssign` | 为用户分配角色(改造现有用户管理) | 1天 | +| 区域管理 | `/system/region` | 省市区管理 + 绑定运营经理 | 1天 | +| 调度面板 | `/dispatch/board` | 待接单订单列表 + 手动指派陪诊师 | 2天 | +| 推广管理 | `/promotion/list` | 推广员列表 + 邀请数据 + 佣金记录 | 1.5天 | +| 分润管理 | `/finance/profit` | 全角色分润记录 + 结算审核 + 批量打款 | 2天 | +| 客服工单 | `/service/ticket` | 退款审核 + 纠纷处理 + 工单记录 | 1.5天 | +| 运营看板 | `/operations/dashboard` | 区域数据、收入趋势、陪诊师排行 | 2天 | + +### 7.2 微信小程序 — 改造清单 + +**核心思路**:同一小程序 + 自定义 TabBar + 角色驱动 UI,不新建独立小程序。 + +#### 基础设施 + +| 改造项 | 说明 | 工作估量 | +|--------|------|---------| +| 自定义 TabBar | `custom-tab-bar/` 组件,根据 `userType` 返回不同 tabBar 配置(最多5个Tab) | 1天 | +| 注册页改造 | 增加身份选择(我需要服务 / 我提供服务 / 我推广赚佣金),调度/客服/运营不开放自选 | 0.5天 | +| 我的页改造 | 增加"身份管理"入口 + 当前角色标识 + 多角色切换(患者⇄推广员可互切) | 0.5天 | +| 首页改造 | 根据角色展示不同功能入口(患者看服务/推广员看数据/工作角色看工单) | 1天 | + +#### 推广端子包 `pages-promoter/`(新增) + +| 页面 | 说明 | 工作估量 | +|------|------|---------| +| 推广数据看板 | 累计邀请人数、推广订单数、佣金总额、趋势图表 | 1.5天 | +| 我的团队 | 下级推广员列表(一级/二级/三级),含每个人的邀请数和贡献佣金 | 1天 | +| 佣金明细 | 每笔佣金来源(哪个用户、哪笔订单)、金额、状态(待结算/已到账) | 1天 | +| 提现页面 | 佣金提现到微信零钱(复用现有 `withdrawal` 页面逻辑) | 0.5天 | +| 分享工具 | 生成带邀请码的分享海报/小程序码,一键转发好友/群 | 1天 | + +#### 工作台子包 `pages-work/`(新增,调度+客服+运营共用) + +| 页面 | 说明 | 工作估量 | +|------|------|---------| +| 调度面板 | 待接单列表 + 点击指派陪诊师(弹窗选择) + 改派 + 超时提醒 | 2天 | +| 退款审核 | 退款申请列表 + 通过/驳回 + 驳回原因填写 + 退款进度跟踪 | 1.5天 | +| 运营看板 | 区域订单数/收入/陪诊师排行(可先用 WebView 加载后台页面快速上线) | 1~2天 | +| 订单搜索 | 全平台订单搜索 + 状态筛选 + 详情查看(复用现有订单列表组件) | 1天 | + +#### 小程序端合计:约 11~13 天 + +### 7.3 Android App — 改造清单 + +**核心思路**:现有陪护 App 保持不动,通过登录后角色判断动态加载不同 Fragment 和 Tab。陪诊师/调度/客服/运营四种角色共用同一 APK。 + +| 改造项 | 说明 | 工作估量 | +|--------|------|---------| +| `MainActivity` 动态化 | 当前写死 3 个 Tab(首页/客户/我的),改为根据 `userType` 组装不同的 Fragment 集合和底部导航 | 1天 | +| 角色判断入口 | `SplashActivity` → login 成功 → 后端返回 userType → 存入本地 → MainActivity 读取 | 0.5天 | +| 调度员界面 | 新增 `DispatchFragment`:待派单列表 + "指派"按钮 + 陪诊师选择弹窗 + 改派确认 | 2天 | +| 客服界面 | 新增 `RefundFragment`:退款审核列表 + 通过/驳回 + 原因填写弹窗 | 1.5天 | +| 运营看板 | 新增 `OpsFragment`:用 WebView 加载后台运营看板页面(快速上线方案) | 0.5天 | +| 订单列表复用 | `TraceFragment` 的 5 个子 Tab 已有完整订单管理,调度/客服/运营可直接复用 | 0天 | +| 网络层 | 新增调度/退款/统计相关 API 调用(在 `HttpConstants` 中补充 URL) | 0.5天 | +| 收入明细 | MyFragment 增加多角色收益展示(陪诊服务费 + 调度费 + 推广佣金等分项列示) | 1天 | + +#### Android 端合计:约 7 天 + +> **不需要两套 App。** 一份代码,一份 APK,登录后根据角色展开不同界面。推广员不进 Android App,统一走微信小程序。 + +--- + +## 八、实施路线图 + +### 阶段 0:前置条件(当前 → 4周) + +在做多角色之前,先完成商业化评估报告中的 P0 安全问题: + +``` +□ JWT 密钥强化 +□ 支付密钥迁移环境变量 +□ 生产环境 Druid/Swagger 关闭 +□ 短信接口限流 +□ 日志脱敏(密码/手机号) +``` +**耗时:2周,1人** + +### 阶段 1:角色基础设施(第 5~8 周) + +``` +□ 数据库表创建(推广关系/分润记录/区域/配置) +□ sys_user 字段扩展(invite_code, parent_promoter_id, region_id) +□ 后端:推广服务、分润引擎、调度服务、区域服务 +□ 管理后台:角色分配、区域管理 +□ 小程序:re注册页身份选择 + 自定义 TabBar 框架 +□ Android:MainActivity 动态 Tab 重构 + 角色判断入口 +``` +**耗时:4周,2人(1后端 + 1全栈)** + +### 阶段 2:推广 + 分润闭环(第 9~11 周) + +``` +□ 后端:分润计算引擎接入支付回调 +□ 后端:推广佣金自动结算 +□ 管理后台:推广管理、分润管理 +□ 小程序:推广端子包(数据看板 + 团队 + 佣金 + 提现 + 分享海报) +□ Android:收入明细增加多角色分项展示 +□ 联调测试:下单 → 支付 → 分润计算 → 推广佣金生成 +``` +**耗时:3周,2人** + +### 阶段 3:调度 + 客服上线(第 12~14 周) + +``` +□ 管理后台:调度面板 + 客服工单 +□ 后端:调度派单逻辑 +□ 小程序:工作台子包 — 调度面板 + 退款审核 +□ Android:调度员界面(DispatchFragment) + 客服界面(RefundFragment) +□ 联调:手动派单完整流程 + 退款审核完整流程 +``` +**耗时:3周,2人** + +### 阶段 4:运营看板 + 全线联调(第 15~16 周) + +``` +□ 管理后台:运营看板(区域数据可视化) +□ 小程序:运营看板(可用 WebView 快速上线) +□ Android:运营看板(WebView 加载后台页面) +□ 全角色端到端测试(患者→陪诊师 + 推广员三级 + 调度派单 + 客服退款) +□ 性能优化(分润计算批量处理) +□ 文档 + 培训材料 +``` +**耗时:2周,2人** + +### 总工期 + +| 阶段 | 内容 | 时间 | +|------|------|------| +| 阶段 0 | 安全整改 | 2 周 | +| 阶段 1 | 角色基础设施 | 4 周 | +| 阶段 2 | 推广 + 分润闭环 | 3 周 | +| 阶段 3 | 调度 + 客服 | 3 周 | +| 阶段 4 | 运营看板 + 联调 | 2 周 | +| **合计** | | **14 周(约 3.5 个月)** | + +人员配置:2 人(1 后端 + 1 全栈/小程序/Android),约 **28 人周 = 7 人月**。 +费用估算:**10~18 万 RMB**(2人 × 3.5月)。 + +--- + +## 九、风险与应对 + +| 风险 | 影响 | 概率 | 应对措施 | +|------|------|------|---------| +| 分润金额计算错误 | 资金纠纷 | 中 | 多级对账:系统计算 + 管理员审核 + 月度导出报表 | +| 推广刷单薅羊毛 | 资金损失 | 高 | 反作弊:同设备/IP限制、最小提现金额、订单完成才结算 | +| 三级分润法律风险 | 被认定为传销 | 低 | 严格限制三级、不设人头费只按消费分润、法务审核 | +| 陪诊师抵制调度员分润 | 运营阻力 | 中 | 调度分润从平台抽成出,不影响陪诊师收入 | +| 角色权限越权 | 数据泄露 | 中 | RuoYi RBAC 已有基础,需逐个接口加 @PreAuthorize | +| 推广佣金过高挤占利润 | 平台亏损 | 中 | 所有比例可配置、设总佣金上限(10%)、定期分析 ROI | + +--- + +## 十、总结 + +### 核心价值 + +1. **从 2 角色 → 7 角色**:将平台的劳动分工精细化,每个人都能在平台上找到自己的位置 +2. **推广分润 = 增长引擎**:三级分润让每个用户都有动力去邀请新用户,实现病毒式增长 +3. **调度运营 = 规模化基础**:有了调度和运营角色,才能跨城市复制,而不是依赖单一管理员 +4. **分润透明 = 信任基础**:每笔订单的分润记录清晰可查,所有角色都能看到自己的贡献和回报 +5. **终端最大化复用**:不新建独立小程序或 App,通过自定义 TabBar + 角色驱动 UI,一份代码服务全部角色 + +### 推荐策略 + +**先做推广员 + 分润引擎(阶段 1+2),再做调度 + 客服(阶段 3)。** 理由是: + +- 推广员角色直接带来用户增长,ROI 最快 +- 分润引擎是整个体系的核心基础设施,先建好再扩展其他角色 +- 调度和客服是成本中心,推广是利润中心,先利润后成本 + +**终端策略:** + +| 终端 | 策略 | +|------|------| +| 微信小程序 | 同一小程序 + 自定义 TabBar + 2 个新子包(`pages-promoter` + `pages-work`),不建新应用 | +| Android App | 现有 `peizhen` 工程扩展,陪诊师/调度/客服/运营四角色共用,推广员不进 App | +| Web 后台 | 新增 7 个页面(角色分配/区域管理/调度面板/推广管理/分润管理/客服工单/运营看板) | + +### 关键成功指标 + +| 指标 | 目标(上线后 3 个月) | +|------|---------------------| +| 推广员注册数 | ≥ 100 人 | +| 推广带来的订单占比 | ≥ 30% | +| 推广获客成本(CAC) | < 订单金额的 10% | +| 调度员处理的订单占比 | ≥ 20% | +| 客服工单处理时效 | < 2 小时 | +| 分润计算准确率 | 100%(经人工审核确认) | + +--- + +> **相关文档**: +> - 商业化评估报告:`D:\androidPj\rlz\docs\商业化评估报告.md` +> - 陪诊员服务费分账方案:`D:\androidPj\rlz\docs\陪诊员服务费分账方案.md` +> - 订单流转设计方案:`D:\androidPj\rlz\docs\订单流转设计方案.md` +> - 后台管理功能模块规划:`D:\androidPj\rlz\docs\后台管理功能模块规划.md` +> - PRD 文档:`D:\androidPj\rlz\docs\项目功能的prd文档.md` diff --git a/docs/多角色系统需求目录.md b/docs/多角色系统需求目录.md new file mode 100644 index 0000000..ada001a --- /dev/null +++ b/docs/多角色系统需求目录.md @@ -0,0 +1,189 @@ +# 多角色系统与分润 — 需求拆解目录 + +> 基于《多角色系统与分润方案.md》 +> 用于任务分配和进度追踪 + +--- + +## 1. 安全整改(前置) + +- [ ] 1.1 JWT 签名密钥强化 → 迁移到环境变量,使用强随机密钥 +- [ ] 1.2 微信支付密钥迁移 → APIv3 Key / RSA 私钥移出源码,从环境变量读取 +- [ ] 1.3 生产环境加固 → 关闭 Druid 监控面板 / Swagger API 文档的外网访问 +- [ ] 1.4 短信接口限流 → 单手机号每分钟 N 次,单 IP 每日 N 次 +- [ ] 1.5 日志脱敏 → `sys_oper_log` 和 `System.out.print` 过滤手机号、密码、验证码 + +--- + +## 2. 数据库 + +### 2.1 新建表 +- [ ] 2.1.1 `rlz_promotion_relation` — 推广关系表(promoter_id, invited_user_id, level, parent_promoter_id) +- [ ] 2.1.2 `rlz_profit_record` — 分润记录表(order_id, user_id, role_type, profit_type, amount, rate, status) +- [ ] 2.1.3 `rlz_promoter_config` — 推广佣金配置表(config_key, config_value, description) +- [ ] 2.1.4 `rlz_region` — 运营区域表(name, parent_id, level, manager_id, status) + +### 2.2 现有表扩展 +- [ ] 2.2.1 `sys_user` — userType 枚举扩展(00/03/04/05/06/C/B),新增 promotion_code、parent_promoter_id、region_id +- [ ] 2.2.2 `rlz_order` — 新增 promoter_id、dispatch_type(auto/manual)、dispatcher_id +- [ ] 2.2.3 `sys_menu` — 新增推广端、调度端、客服端、运营端菜单项 +- [ ] 2.2.4 `sys_role` — 新增运营经理(03)、客服(04)、调度(05)、推广员(06) 角色记录 + +--- + +## 3. 后端(Spring Boot) + +### 3.1 推广服务 `RlzPromotionService` +- [ ] 3.1.1 生成唯一邀请码接口 +- [ ] 3.1.2 绑定推广关系接口(扫码/链接 → 写入 rlz_promotion_relation) +- [ ] 3.1.3 查询推广数据接口(邀请人数、团队层级、推广订单数) +- [ ] 3.1.4 查询推广团队接口(一/二/三级下级列表) + +### 3.2 分润引擎 `RlzProfitService` +- [ ] 3.2.1 分润计算核心逻辑(陪诊师 80% + 调度 1~2% + 推广三级 5/3/2%) +- [ ] 3.2.2 支付回调集成 → 订单支付成功自动触发分润计算 +- [ ] 3.2.3 分润记录写入 `rlz_profit_record` +- [ ] 3.2.4 分润结算接口(管理员确认 → 更新各角色余额 → 生成 jiaoyi_detail) +- [ ] 3.2.5 分润记录查询接口(按角色/订单/时间筛选) +- [ ] 3.2.6 运营经理区域流水月结逻辑 + +### 3.3 调度服务 `RlzDispatchService` +- [ ] 3.3.1 待接单订单列表接口(status=0) +- [ ] 3.3.2 手动指派陪诊师接口(绑定 bId + dispatch_type=manual + dispatcher_id) +- [ ] 3.3.3 改派接口(更换 bId,记录改派日志) +- [ ] 3.3.4 区域内的陪诊师列表接口(供调度选择) + +### 3.4 区域服务 `RlzRegionService` +- [ ] 3.4.1 区域 CRUD(省/市/区三级) +- [ ] 3.4.2 区域绑定运营经理 +- [ ] 3.4.3 区域订单/流水统计接口 + +### 3.5 权限改造 +- [ ] 3.5.1 各角色 `@PreAuthorize` 注解补充(目前约 95% 接口无细粒度权限) +- [ ] 3.5.2 数据权限隔离(运营经理只看本区域数据,陪诊师只看自己的订单) +- [ ] 3.5.3 小程序端接口角色校验(userType 匹配检查) + +### 3.6 新增 Controller +- [ ] 3.6.1 `RlzPromotionController` — /system/promotion/* +- [ ] 3.6.2 `RlzProfitController` — /system/profit/* +- [ ] 3.6.3 `RlzDispatchController` — /system/dispatch/* +- [ ] 3.6.4 `RlzRegionController` — /system/region/* + +--- + +## 4. 管理后台(Vue2) + +### 4.1 角色与权限 +- [ ] 4.1.1 用户管理 → 增加 userType 多选下拉筛选(全角色) +- [ ] 4.1.2 用户管理 → 新增/编辑用户时可分配多角色 +- [ ] 4.1.3 角色管理 → 新增运营经理/客服/调度/推广员角色及对应菜单权限 + +### 4.2 推广管理 +- [ ] 4.2.1 推广员列表页(搜索、筛选、邀请人数、推广订单数、佣金总额) +- [ ] 4.2.2 推广关系树查看(某个推广员的上下级关系图) +- [ ] 4.2.3 推广佣金记录(按推广员/时间/订单查看佣金明细) + +### 4.3 调度管理 +- [ ] 4.3.1 调度面板(待接单列表 + 区域筛选 + 超时标红) +- [ ] 4.3.2 手动指派弹窗(选择区域内空闲陪诊师 + 确认指派) +- [ ] 4.3.3 改派操作(已指派订单 → 更换陪诊师 + 原因记录) + +### 4.4 客服工单 +- [ ] 4.4.1 退款审核列表(status=5 申请退款订单) +- [ ] 4.4.2 退款审核操作(通过 → status=6 退款中 / 驳回 → status 回退 + 驳回原因) +- [ ] 4.4.3 工单处理记录(每笔工单的处理人、处理时间、处理结果) + +### 4.5 财务扩展 +- [ ] 4.5.1 分润管理页(全角色分润记录 + 筛选 + 汇总统计) +- [ ] 4.5.2 结算管理页(待结算列表 + 单个/批量结算 + 打款确认) +- [ ] 4.5.3 平台配置页(推广佣金比例、调度分润比例、运营分润比例、自动结算开关) + +### 4.6 运营看板 +- [ ] 4.6.1 区域数据总览(各区域订单量、流水、陪诊师数、推广员数) +- [ ] 4.6.2 订单趋势图(按日/周/月聚合,折线图) +- [ ] 4.6.3 陪诊师服务排行(柱状图,按接单数/收入排序) +- [ ] 4.6.4 推广效果排行(邀请人数/推广收入排行) + +### 4.7 区域管理 +- [ ] 4.7.1 区域列表页(省/市/区三级树形结构) +- [ ] 4.7.2 区域新增/编辑/删除 + 绑定运营经理 + +--- + +## 5. 微信小程序 + +### 5.1 基础设施 +- [ ] 5.1.1 自定义 TabBar 组件(`custom-tab-bar/`,根据 userType 动态渲染底部导航) +- [ ] 5.1.2 注册页 → 增加身份选择(我需要服务 / 我提供服务 / 我推广赚佣金) +- [ ] 5.1.3 我的页 → 增加"身份管理"入口 + 当前角色标识 + 角色切换 +- [ ] 5.1.4 首页 → 根据 userType 展示不同的首页内容 +- [ ] 5.1.5 `app.json` → 注册新子包路径(pages-promoter、pages-work) + +### 5.2 推广端子包 `pages-promoter/` +- [ ] 5.2.1 推广数据看板 → 累计邀请人数、推广订单数、佣金总额、趋势简图 +- [ ] 5.2.2 我的团队 → 一/二/三级下级推广员列表 + 每人邀请数 + 贡献佣金 +- [ ] 5.2.3 佣金明细 → 每笔佣金来源(用户+订单)、金额、状态(待结算/已到账) +- [ ] 5.2.4 提现 → 佣金提现到微信零钱(复用现有 withdrawal 页面逻辑) +- [ ] 5.2.5 分享工具 → 生成带邀请码的分享海报/小程序码,一键转发好友/群 + +### 5.3 工作台子包 `pages-work/` +- [ ] 5.3.1 调度面板 → 待接单列表 + 点击指派陪诊师弹窗 + 改派 + 超时提醒 +- [ ] 5.3.2 退款审核 → 退款申请列表 + 通过/驳回 + 驳回原因填写 + 进度跟踪 +- [ ] 5.3.3 运营看板 → 区域数据(订单数/收入/陪诊师排行),可先用 WebView 加载后台页面 +- [ ] 5.3.4 订单搜索 → 全平台订单搜索 + 状态筛选 + 详情查看 + +### 5.4 陪诊师轻量入口(现有页面改造) +- [ ] 5.4.1 陪诊师 Tab → 待接单列表 / 进行中 / 已完成,三个子 Tab +- [ ] 5.4.2 接单/拒单操作 → 复用现有接口,增加确认弹窗 +- [ ] 5.4.3 开始服务 / 完成服务 → 状态流转按钮 +- [ ] 5.4.4 陪诊师收入 → 收益明细 + 余额 + 提现入口 + +--- + +## 6. Android App(peizhen 工程) + +### 6.1 框架改造 +- [ ] 6.1.1 `MainActivity` 动态 Tab → 从写死 3 个改为根据 userType 组装 Fragment 集合 +- [ ] 6.1.2 角色判断入口 → `SplashActivity` → login → 后端返回 userType → 决定加载模式 +- [ ] 6.1.3 `MyFragment` 改造 → 增加多角色收益分项展示(服务费/调度费/推广佣金) + +### 6.2 调度员界面 +- [ ] 6.2.1 `DispatchFragment` → 待派单列表(status=0 订单) +- [ ] 6.2.2 指派弹窗 → 选择区域内空闲陪诊师 + 确认指派 +- [ ] 6.2.3 改派操作 → 已指派订单更换陪诊师 + 原因 + +### 6.3 客服界面 +- [ ] 6.3.1 `RefundFragment` → 退款审核列表(status=5 订单) +- [ ] 6.3.2 退款处理 → 通过/驳回 + 原因填写弹窗 + +### 6.4 运营看板 +- [ ] 6.4.1 `OpsFragment` → WebView 加载后台运营看板页面(快速上线方案) + +### 6.5 网络层 +- [ ] 6.5.1 `HttpConstants` → 补充调度/退款/统计相关 API URL +- [ ] 6.5.2 请求/响应实体类 → dispatch、refund、profit 相关 Bean + +--- + +## 7. 联调与测试 + +- [ ] 7.1 患者下单 → 支付 → 分润计算 → 各角色收益生成(端到端) +- [ ] 7.2 推广员三级关系 → 下单 → 三级佣金正确分配 +- [ ] 7.3 调度员手动派单 → 陪诊师接单 → 完成服务 → 分润正确 +- [ ] 7.4 退款审核流程 → 客服通过/驳回 → 状态流转正确 +- [ ] 7.5 运营经理查看区域数据 → 数据与订单实际一致 +- [ ] 7.6 多角色切换 → TabBar 正确切换 → 权限隔离生效 +- [ ] 7.7 提现流程 → 各角色钱包操作正确 → 微信打款到账 + +--- + +## 8. 文档与部署 + +- [ ] 8.1 运维文档(建表 DDL、配置项说明、角色权限矩阵表) +- [ ] 8.2 用户操作手册(各角色小程序/App/后台使用说明) +- [ ] 8.3 部署上线(后端 docker build + 小程序上传审核 + Android 打包) + +--- + +> **统计**:共 75 项子任务,按阶段 0~4 顺序执行。 +> 详细方案见 `多角色系统与分润方案.md` diff --git a/docs/用户操作手册.md b/docs/用户操作手册.md new file mode 100644 index 0000000..011cd46 --- /dev/null +++ b/docs/用户操作手册.md @@ -0,0 +1,328 @@ +# 瑞来健康 — 用户操作手册 + +> 更新日期: 2026-06-08 +> 适用版本: v2.0 多角色系统 + +--- + +## 目录 + +1. [患者/客户 — 小程序端](#1-患者客户--小程序端) +2. [陪诊师 — 小程序端 & Android App](#2-陪诊师--小程序端和-android-app) +3. [推广员 — 小程序端](#3-推广员--小程序端) +4. [调度员 — 小程序端](#4-调度员--小程序端) +5. [客服专员 — 小程序端](#5-客服专员--小程序端) +6. [运营经理 — 小程序端 & 管理后台](#6-运营经理--小程序端和管理后台) +7. [超级管理员 — 管理后台](#7-超级管理员--管理后台) + +--- + +## 1. 患者/客户 — 小程序端 + +**适用角色**: user_type=00 (默认注册用户) + +### 1.1 注册与登录 + +1. 打开微信小程序 "瑞来健康" +2. 点击 "我的" → "登录/注册" +3. 输入手机号 → 获取验证码 → 验证通过 +4. 首次登录自动创建账户, user_type=00 + +### 1.2 浏览与下单 + +1. **首页**: 查看医院列表和服务项目 +2. **搜索医院**: 按区域/名称筛选 +3. **选择服务**: 点击服务类型(全程陪诊/取报告/代问诊等) +4. **下单**: + - 选择日期、时间段 + - 填写就诊人信息 + - 选择支付方式(微信支付) + - 确认订单 +5. **支付**: 调用微信支付完成付款 + +### 1.3 订单管理 + +| 状态码 | 状态名称 | 说明 | +|--------|----------|------| +| 0 | 待派单 | 支付成功, 等待分配陪诊师 | +| 1 | 已接单 | 陪诊师已接单 | +| 2 | 服务中 | 陪诊师已开始服务 | +| 3 | 待确认 | 服务完成待确认 | +| 4 | 已完成 | 服务已完成 | +| 5 | 申请退款 | 已提交退款申请 | +| 7 | 已退款 | 退款已完成 | +| 8 | 已结算 | 已完成结算 | + +操作: +- **查看订单**: "我的" → "我的订单" +- **查看详情**: 点击具体订单进入详情页 +- **申请退款** (status=4 或 8 可见): 订单详情 → "申请退款" → 选择原因 → 提交 +- **退款进度**: 订单详情查看退款状态 + +### 1.4 我的页面 + +- 个人资料修改(头像、昵称) +- 实名认证(陪诊服务需要) +- 余额查看与提现 +- 联系客服 + +--- + +## 2. 陪诊师 — 小程序端和 Android App + +**适用角色**: user_type=01 +**前置条件**: 管理员在后台设置用户为陪诊师角色 + +### 2.1 小程序端 + +#### 首页(陪诊师视图) +- 待接单列表(status=0 且已派单给本人) +- 进行中订单 +- 今日收入概览 + +#### 订单操作流程 + +1. **接单**: + - 收到新订单推送 → 进入订单详情 + - 点击 "接单" → 确认弹窗 → 状态变为 1 (已接单) + +2. **开始服务**: + - 到达约定地点 → 订单详情 → "开始服务" → 状态变为 2 (服务中) + +3. **完成服务**: + - 服务结束 → 订单详情 → "完成服务" → 状态变为 3 (待确认) + +4. **拒绝订单**: + - 无法接单 → "拒绝" → 输入原因 → 订单退回待派单 + +#### 收入查看 +- "我的" → "收益明细" → 查看每笔服务费 +- "我的" → "余额" → 可提现到微信零钱 + +### 2.2 Android App + +**包名**: `com.ruoyi.peizhen` + +1. 登录后根据 user_type 自动加载陪诊师界面 +2. **订单 Tab**: 待接单 / 进行中 / 已完成 三个子 Tab +3. **接单/拒单**: 点击订单 → 操作按钮 +4. **开始/完成服务**: 按状态流转操作 +5. **我的 Tab**: 收入展示、余额、提现 + +--- + +## 3. 推广员 — 小程序端 + +**适用角色**: user_type=06 + +### 3.1 推广看板 + +底部 Tab "推广" → 进入 `pages-promoter` 子包: +- **累计数据**: 邀请人数、推广订单数、佣金总额 +- **今日概览**: 今日新增邀请、今日佣金 + +### 3.2 我的团队 + +- 查看一级/二级/三级下级推广员列表 +- 每人显示: 邀请人数、贡献佣金 +- 点击可查看该推广员的详细数据 + +### 3.3 佣金明细 + +- 每笔佣金来源: 关联用户 + 订单号 +- 金额: 订单金额 × 推广比例(一级5%/二级3%/三级2%) +- 状态: 待结算 / 已到账 + +### 3.4 分享邀请 + +1. "推广" → "邀请好友" +2. 系统自动生成唯一邀请码 +3. 分享方式: + - 生成带邀请码的海报 + - 分享小程序卡片到微信群 + - 复制邀请链接发送 + +被邀请人通过链接注册后, 自动绑定推广关系。 + +### 3.5 提现 + +- "我的" → "余额" → "提现" +- 可选择提现到微信零钱 +- 提现记录可查看 + +--- + +## 4. 调度员 — 小程序端 + +**适用角色**: user_type=05 + +### 4.1 调度面板 + +底部 Tab "工作台" → 调度面板: +- 待派单订单列表(status=0) +- 按区域筛选 +- 超时未派单订单标红提醒 + +### 4.2 手动派单 + +1. 点击待派单订单 → 进入调度详情 +2. 点击 "指派陪诊师" +3. 弹出区域内空闲陪诊师列表 +4. 选择陪诊师 → 确认指派 +5. 订单状态变为 1(已接单), dispatch_type=manual + +### 4.3 改派操作 + +1. 已指派订单(status=1/2) → 点击 "改派" +2. 选择新陪诊师 + 填写改派原因 +3. 确认 → 订单更新为新的 bId + +### 4.4 派单历史 + +- 查看自己经手的所有派单记录 +- 包括指派时间、订单状态 + +--- + +## 5. 客服专员 — 小程序端 + +**适用角色**: user_type=04 + +### 5.1 退款审核列表 + +底部 Tab "工作台" → 退款审核: +- 所有 status=5 (申请退款) 的订单 +- 显示: 订单号、用户、金额、申请时间、退款原因 + +### 5.2 退款处理 + +**通过退款**: +1. 点击订单 → "审核通过" +2. 系统自动调用微信退款接口 +3. 状态: 5 → 6 (退款中) → 7 (已退款) +4. 退款成功后, 自动回退分润记录 + +**驳回退款**: +1. 点击订单 → "驳回" +2. 填写驳回原因(必填) +3. 状态: 5 → 4 (已完成), 驳回原因写入 remark +4. 用户可在订单详情查看驳回原因 + +### 5.3 工单记录 + +- 每笔工单记录处理人、处理时间、处理结果 +- 可在退款审核列表查看处理历史 + +--- + +## 6. 运营经理 — 小程序端和管理后台 + +**适用角色**: user_type=03 + +### 6.1 小程序端 — 运营看板 + +底部 Tab "工作台" → 运营看板(WebView 加载): +- 本区域订单量、流水、陪诊师数、推广员数 +- 订单趋势图(按日/周/月) +- 陪诊师服务排行 +- 推广效果排行 + +### 6.2 管理后台 + +登录地址: `http://101.43.95.130:8050` + +运营经理可见菜单: +- **推广管理**: 查看所有推广员、推广关系、佣金记录 +- **区域管理**: 查看本区域及下级区域数据 +- **调度管理**: 查看调度面板和派单历史 +- **财务管理**: 退款审核、结算管理、平台配置、分润记录 +- **运营看板**: 区域数据汇总 + +### 6.3 区域数据 + +- 只能查看本 region_id 及子区域的数据 +- 支持按时间范围筛选 +- 支持导出报表 + +--- + +## 7. 超级管理员 — 管理后台 + +**适用角色**: admin (role_id=1) + +### 7.1 登录 + +1. 访问 `http://101.43.95.130:8050` +2. 输入账号密码 + 验证码 +3. 登录后进入管理后台首页 + +### 7.2 主要功能模块 + +#### 系统管理 +- **用户管理**: CRUD + 分配角色 + 设置 user_type +- **角色管理**: 角色 CRUD + 分配菜单权限 +- **菜单管理**: 菜单树管理 +- **医院管理**: 合作医院信息维护 +- **服务类型**: 服务项目管理(含图标上传) +- **区域管理**: 省/市/区三级区域维护 + 绑定运营经理 + +#### 推广管理 +- 推广员列表: 查看所有推广员及数据 +- 推广关系: 查看某推广员的上下级树 +- 佣金明细: 按推广员/时间/订单查看 + +#### 调度管理 +- 调度面板: 待派单列表 + 手动指派 + 改派 +- 派单历史: 所有调度操作记录 + +#### 财务管理 +- **退款审核**: 待审核退款列表 + 通过/驳回 +- **结算管理**: 待结算列表 + 单笔/批量结算 +- **分润记录**: 全角色分润记录 + 筛选 + 汇总 +- **平台配置**: 佣金比例/调度比例/运营比例/自动结算开关 +- **收益明细**: 各角色收入流水 + +#### 运营看板 +- 区域数据总览 +- 订单趋势图 +- 陪诊师服务排行 +- 推广效果排行 + +### 7.3 常用操作流程 + +**新增陪诊师**: +1. 系统管理 → 用户管理 → 新增 +2. 填写基本信息 → user_type 选择 "01-陪诊师" +3. 角色分配 → 保存 + +**分配运营区域**: +1. 系统管理 → 区域管理 → 新增/编辑区域 +2. 选择 manager_id(运营经理) +3. 运营经理登录后可看到本区域数据 + +**处理退款**: +1. 财务管理 → 退款审核 +2. 查看 status=5 的订单 +3. 通过: 系统调微信退款 → status 5→6→7 +4. 驳回: 填写原因 → status 5→4 + +**查看分润**: +1. 财务管理 → 分润记录 +2. 可按角色/订单/时间筛选 +3. 查看每个订单的分润分配详情 + +--- + +## 附录: 常见问题 + +**Q: 注册后为什么看不到推广入口?** +A: 需要在管理后台将用户的 user_type 设置为 06(推广员), 并分配 promoter 角色。 + +**Q: 陪诊师如何接单?** +A: 调度员手动指派后, 订单会出现在陪诊师的待接单列表。自动派单的订单直接进入陪诊师列表。 + +**Q: 提现多久到账?** +A: 提现申请后由管理员审核结算, 通过微信企业付款到零钱, 一般 1-2 个工作日。 + +**Q: 退出订单如何恢复?** +A: 客服驳回退款后, 订单状态恢复到"已完成"(4), 可正常结算。 diff --git a/docs/竞争力评估分析.md b/docs/竞争力评估分析.md new file mode 100644 index 0000000..6fea824 --- /dev/null +++ b/docs/竞争力评估分析.md @@ -0,0 +1,184 @@ +# 多角色系统竞争力评估 + +> 评估角度:从当前 B-C 双边平台 → 七角色多利益方平台,竞争力的量级跃迁 +> 日期:2026-06-07 + +--- + +## 一、当前平台 vs 方案落地后:关键指标对比 + +| 维度 | 当前(v1.0) | 落地后(v2.0) | 提升 | +|------|------------|-------------|------| +| 角色数 | 3(管理员/患者/陪诊师) | 7 | +4 | +| 增长模式 | 自然流量,无裂变机制 | 推广员三级分润病毒式增长 | 从0到1 | +| 供给侧获取 | 陪诊师需下载App+实名认证 | 小程序轻量入口+App双通道 | 降低70%门槛 | +| 派单方式 | 仅自动派单,无人工兜底 | 自动+手动调度双模式 | 补齐关键缺失 | +| 纠纷处理 | 管理员一人处理 | 客服专员独立角色 | 从瓶颈到专人 | +| 跨城市扩展 | 无区域概念 | 运营经理+区域管理体系 | 从0到1 | +| 用户留存 | 单角色,用完即走 | 多角色身份,切换成本高 | 粘性数倍提升 | +| 分润透明 | 仅陪诊师服务费 | 全角色分润记录+实时可查 | 信任度质变 | +| 收入结构 | 单一平台抽成20% | 平台抽成+推广佣金预留+区域流水分润 | 多元化 | +| 竞品壁垒 | 低(可复制代码) | 高(需复制生态系统+利益网络) | 质变 | + +--- + +## 二、竞争力提升的核心逻辑 + +### 2.1 网络效应维度 + +``` +当前:双边网络(患者 ↔ 陪诊师) + —— 线性增长,每获取一个用户/陪诊师都需要平台自己投入 + +落地后:多边网络 + 患者 + ↑↓ + 陪诊师 ←→ 调度员(优化匹配效率) + ↑↓ + 推广员 ←→ 新患者(裂变增长) + ↑↓ + 客服(保障体验) + ↑↓ + 运营经理(跨城市复制) +``` + +**关键变化**:从平台自己拉双边 → 推广员拉患者、调度员优化匹配、运营经理开新城。增长引擎从"推"变"拉"。 + +### 2.2 获客成本(CAC)维度 + +| 获客方式 | 当前 | 落地后 | +|---------|------|--------| +| 自然搜索/口碑 | 主要方式 | 辅助 | +| 推广员三级分润 | 无 | **核心引擎** | +| CAC(每付费用户) | 50~200 元(广告投放) | 订单金额 × 10%(推广佣金),**不成交不付费** | +| 获客风险 | 预付费广告,ROI不确定 | 后付费佣金,ROI确定 | + +**结论**:从"先花钱买量"变成"成交后分润",获客模型从高风险变成零风险。 + +### 2.3 供给端(陪诊师)维度 + +| | 当前 | 落地后 | +|------|------|--------| +| 注册路径 | 下载App → 注册 → 实名认证 | **小程序即开即用** + App完整体验 | +| 新手体验 | 下载等5分钟,放弃率~60% | 扫码即注册,1分钟开始接单 | +| 收入激励 | 单一服务费 | 服务费 + **推广佣金**(陪诊师也可推广) | +| 月留存 | 依赖订单量 | 身份切换成本高,多收入来源,留存提升 | + +**陪诊师供给量预估**:轻量入口可提升 3~5 倍注册转化。 + +### 2.4 规模复制维度 + +``` +当前:一个城市 = 一个管理员包揽一切 + 扩张到 5 个城市 → 管理员瓶颈 → 系统崩溃 + +落地后:一个城市 = 运营经理(1人) + 调度员(1~2人) + 客服(1人) + 推广员(N人) + 陪诊师(N人) + 扩张到 N 个城市 → 每个城市独立运营团队 → 线性扩展 +``` + +从"创始人驱动"变成"系统驱动",具备了 SaaS 平台连锁扩张的能力。 + +### 2.5 用户生命周期价值(LTV)维度 + +| 角色 | 当前 LTV | 落地后 LTV | +|------|---------|----------| +| 患者 | 单次消费 200~500 元 | 被推广后持续消费 + 可能成为推广员 | +| 陪诊师 | 服务费收入 | 服务费 + 推广佣金,总收入提升 | +| 推广员 | 不存在 | 持续分润,月入数千元可能性 | +| **平台 LTV/CAC 比** | 估计 2:1 | 估计 **5:1 ~ 10:1** | + +--- + +## 三、与竞品的差异化对比 + +假设国内主流陪诊竞品(如 e陪诊、安心陪诊、贴心陪诊等): + +| 能力 | 典型竞品 | 瑞来落地后 | 是否差异化 | +|------|---------|----------|:--:| +| 患者端小程序 | ✅ | ✅ | - | +| 陪诊师 Android App | ✅ | ✅ | - | +| 派单调度 | 部分有 | ✅ 自动+手动 | 略优 | +| 分润结算 | 部分有 | ✅ 全角色透明分润 | **差异** | +| 推广裂变体系 | 极少 | ✅ 三级分润 | **核心差异** | +| 区域运营管理 | 极少 | ✅ 运营经理+区域 | **核心差异** | +| 客服工单系统 | 部分有 | ✅ 独立客服角色 | 略优 | +| 多端多角色统一 | 无 | ✅ 一小程序服务全角色 | **壁垒** | +| 陪诊师轻量入口 | 极少 | ✅ 小程序+App双通道 | **差异** | + +**核心护城河**:竞品可以抄代码,但抄不了"推广员利益网络"——一旦推广员体系建立,推广员和他们的下级关系构成巨大的迁移成本。这类似于美团饿了么的地推网络、滴滴的司机网络。 + +--- + +## 四、量化估算 + +### 4.1 增长模型(保守估计) + +假设平台当前月订单 500 单: + +| 阶段 | 时间 | 月订单 | 增长驱动 | +|------|------|--------|---------| +| 当前 | 第0月 | 500 | 自然流量 | +| 推广员上线 | 第3月 | 1,000 | 首批 20 个推广员,人均引流 25 单 | +| 推广员裂变 | 第6月 | 2,500 | 推广员 100 人,三级裂变生效 | +| 区域复制 | 第12月 | 8,000 | 3 个城市,每城独立推广运营 | +| 规模化 | 第18月 | 20,000+ | 5+ 城市,品牌效应叠加 | + +**12 个月增长 16 倍**,主要通过推广裂变 + 区域复制实现。 + +### 4.2 收入模型 + +假设月订单 8,000 单,均价 500 元/单,月流水 400 万: + +| 收入项 | 金额 | 备注 | +|--------|------|------| +| 平台抽成(20%→扣除推广佣金后) | 40 万 | 减去推广佣金 10% 后的净收入 | +| 推广佣金留存(从 20% 中出) | 已计入 | 三级 5/3/2% | +| 月总收入 | **~40 万** | 仅平台抽成 | +| 运营成本(服务器+短信+运维) | ~2 万 | | +| 城市运营团队成本(3城市×3人) | ~9 万 | 运营经理+客服+调度 | +| 月净利润 | **~29 万** | | + +对比当前:月 500 单 × 500 元 × 20% = 5 万/月,减去运维 1 万,净利润约 4 万/月。 + +**收入从 4 万/月 → 29 万/月,提升约 7 倍**。 + +### 4.3 估值影响 + +对于 SaaS/平台型公司,估值通常基于 GMV 倍数或收入倍数: + +| 指标 | 当前 | 落地后(12个月) | 倍数 | +|------|------|-------------|------| +| 年 GMV | 300 万 | 4,800 万 | 16× | +| 年收入 | 48 万 | ~350 万 | 7× | +| 估值(按 GMV 2×) | 600 万 | 9,600 万 | 16× | +| 估值(按收入 10×) | 480 万 | 3,500 万 | 7× | + +**平台估值从百万级跃升至千万~亿级。** + +--- + +## 五、竞争力提升总结 + +``` + 当前竞争力 落地后竞争力 + │ │ + 技术能力 ████████████ 80% ████████████ 90% + 产品体验 ██████ 50% ██████████ 80% + 供给规模 ████ 30% ██████████ 85% + 用户增长 ██ 15% ██████████ 85% ← 最大提升 + 运营效率 ████ 35% ██████████ 80% + 商业壁垒 ██ 10% ██████████ 90% ← 质变 + 规模化能力 ██ 10% ██████████ 85% ← 从0到1 + 盈利能力 ████ 30% ██████████ 80% + ─────────────────────────────────────────────────────────── + 综合竞争力 ★★☆☆☆ (2/5) ★★★★☆ (4/5) +``` + +### 一句话总结 + +**从"能用的工具"变成"能自我增长的平台"。** 当前平台是一个功能基本可用的 O2O 工具,竞争对手花 3 个月可以复制代码。落地后,推广员利益网络 + 区域运营体系 + 多角色分润生态构成了三重护城河,竞争对手即使拿到源码也无法复制生态。平台估值从百万级跃升至千万~亿级,具备了真正商业化运营和融资的资格。 + +--- + +> **关键前提**:以上估算基于安全整改和核心功能稳定运行的前提。如果支付安全、JWT 等 P0 问题未解决,增长越快风险越大。 +> 参考:商业评估报告 §二(安全问题)、多角色系统与分润方案.md diff --git a/docs/角色权限矩阵.md b/docs/角色权限矩阵.md new file mode 100644 index 0000000..0008a8b --- /dev/null +++ b/docs/角色权限矩阵.md @@ -0,0 +1,178 @@ +# 瑞来健康 — 角色权限矩阵 + +> 更新日期: 2026-06-08 + +--- + +## 一、角色定义 + +| role_id | role_key | 角色名称 | user_type | 适用端 | +|---------|----------|----------|-----------|--------| +| 1 | admin | 超级管理员 | 0C | 管理后台 | +| 2 | common | 普通用户 | 00 | 小程序 | +| 103 | operations | 运营经理 | 03 | 管理后台/小程序 | +| 104 | service | 客服专员 | 04 | 管理后台/小程序 | +| 105 | dispatcher | 调度员 | 05 | 管理后台/小程序 | +| 106 | promoter | 推广员 | 06 | 管理后台/小程序 | +| - | caregiver | 陪诊师 | 01 | 小程序/App | +| - | patient | 患者/客户 | 00 | 小程序 | + +--- + +## 二、用户类型 (user_type) 枚举 + +| user_type | 名称 | 说明 | +|-----------|------|------| +| 00 | 患者/客户 | 需要陪诊服务的用户 | +| 01 | 陪诊师 | 提供陪诊服务的从业人员 | +| 03 | 运营经理 | 管理指定区域业务 | +| 04 | 客服专员 | 处理退款/纠纷工单 | +| 05 | 调度员 | 订单派单与陪诊师调度 | +| 06 | 推广员 | 推广获客赚佣金 | +| 0B | 机构 | 医疗机构/合作伙伴 | +| 0C | 管理员 | 平台超级管理员 | + +--- + +## 三、管理后台菜单权限 + +| 菜单 | 超级管理员 | 运营经理 | 客服 | 调度员 | 推广员 | +|------|:---------:|:------:|:---:|:----:|:-----:| +| 系统管理-用户管理 | O | O | - | - | - | +| 系统管理-角色管理 | O | - | - | - | - | +| 系统管理-菜单管理 | O | - | - | - | - | +| 系统管理-部门管理 | O | - | - | - | - | +| 系统管理-岗位管理 | O | - | - | - | - | +| 系统管理-字典管理 | O | O | - | - | - | +| 系统管理-通知公告 | O | O | O | O | O | +| 系统管理-医院管理 | O | O | - | - | - | +| 系统管理-服务类型 | O | O | - | - | - | +| 推广管理 | O | O | - | - | O | +| 区域管理 | O | O | - | - | - | +| 调度管理 | O | O | - | O | - | +| 财务管理-退款审核 | O | O | O | - | - | +| 财务管理-结算管理 | O | O | - | - | - | +| 财务管理-平台配置 | O | O | - | - | - | +| 财务管理-分润记录 | O | O | O | O | O | +| 财务管理-收益明细 | O | O | - | - | - | +| 运营看板 | O | O | - | - | - | + +O = 有权限, - = 无权限 + +--- + +## 四、API 权限 (@PreAuthorize) + +### 4.1 推广服务 `/system/promotion/**` + +| 端点 | 方法 | 权限 | admin | ops | service | dispatcher | promoter | +|------|------|------|:----:|:---:|:-------:|:---------:|:--------:| +| `/list` | GET | - | O | O | - | - | - | +| `/relations` | GET | - | O | O | - | - | - | +| `/{id}` | GET | - | O | O | - | - | - | +| `/data/{userId}` | GET | - | O | O | - | - | - | +| `/byInvited/{id}` | GET | - | O | O | - | - | - | +| `/byPromoter/{id}` | GET | - | O | O | - | - | - | +| POST `/` | POST | system:promotion:add | O | - | - | - | - | +| PUT `/` | PUT | system:promotion:edit | O | - | - | - | - | +| DELETE `/{ids}` | DELETE | system:promotion:remove | O | - | - | - | - | +| `/myStats` | GET | - | O | O | O | O | O | +| `/team` | GET | - | O | O | O | O | O | +| `/myCommission` | GET | - | O | O | O | O | O | +| `/generateCode` | POST | - | O | O | O | O | O | + +### 4.2 分润服务 `/system/profit/**` + +| 端点 | 方法 | 权限 | admin | ops | service | dispatcher | promoter | +|------|------|------|:----:|:---:|:-------:|:---------:|:--------:| +| `/list` | GET | - | O | O | - | - | - | +| `/stats` | GET | - | O | O | O | - | - | +| `/{id}` | GET | - | O | O | - | - | - | +| `/byOrder/{id}` | GET | - | O | O | O | O | O | +| `/byUser/{id}` | GET | - | O | O | O | O | O | +| `/balance/{id}` | GET | - | O | O | O | O | O | +| `/myRecords` | GET | - | O | O | O | O | O | +| `/myBalance` | GET | - | O | O | O | O | O | +| POST `/calculate/{id}` | POST | system:profit:calculate | O | - | - | - | - | +| POST `/reverse/{id}` | POST | system:profit:reverse | O | - | - | - | - | + +### 4.3 调度服务 `/system/dispatch/**` + +| 端点 | 方法 | 权限 | admin | ops | dispatcher | +|------|------|------|:----:|:---:|:---------:| +| `/pending` | GET | system:dispatch:list | O | O | O | +| `/caregivers` | GET | system:dispatch:list | O | O | O | +| `/caregivers/{hospitalId}` | GET | system:dispatch:list | O | O | O | +| `/history` | GET | system:dispatch:list | O | O | O | +| `/assign/{orderId}/{cgId}` | POST | system:dispatch:assign | O | O | O | + +### 4.4 区域服务 `/system/region/**` + +| 端点 | 方法 | 权限 | admin | ops | +|------|------|------|:----:|:---:| +| `/list` | GET | - | O | O | +| `/stats` | GET | - | O | O | +| `/{id}` | GET | - | O | O | +| `/children/{id}` | GET | - | O | O | +| POST `/` | POST | system:region:add | O | - | +| PUT `/` | PUT | system:region:edit | O | - | +| DELETE `/{ids}` | DELETE | system:region:remove | O | - | + +### 4.5 退款流程 + +| 端点 | 方法 | 权限 | admin | ops | service | +|------|------|------|:----:|:---:|:-------:| +| `/system/order/applyRefund/{id}` | POST | - | O | O | O | +| `/system/order/refundOrder/{id}` | GET | - | O | O | O | +| `/system/view/refundApprove/{id}` | PUT | system:order:edit | O | O | O | + +--- + +## 五、数据权限隔离 + +| 角色 | 数据范围 | +|------|----------| +| 超级管理员 | 全部数据 | +| 运营经理 | 本区域(region_id)及其下级区域数据 | +| 客服专员 | 全部退款工单(无区域限制) | +| 调度员 | 本区域的待派单订单 + 自己的派单记录 | +| 推广员 | 自己的推广关系 + 团队数据 + 佣金记录 | +| 陪诊师 | 自己的订单 + 服务收入 | +| 患者 | 自己的订单 + 支付记录 | + +--- + +## 六、小程序/App 角色功能权限 + +| 功能 | 患者 | 陪诊师 | 推广员 | 调度员 | 客服 | 运营 | +|------|:---:|:----:|:-----:|:-----:|:---:|:---:| +| 浏览服务/下单 | O | - | - | - | - | - | +| 查看我的订单 | O | - | - | - | - | - | +| 支付/退款申请 | O | - | - | - | - | - | +| 接单/拒单 | - | O | - | - | - | - | +| 开始/完成服务 | - | O | - | - | - | - | +| 查看收入/余额 | - | O | O | - | - | - | +| 提现 | - | O | O | - | - | - | +| 推广看板 | - | - | O | - | - | - | +| 我的团队 | - | - | O | - | - | - | +| 佣金明细 | - | - | O | - | - | - | +| 生成邀请码 | - | - | O | - | - | - | +| 调度面板 | - | - | - | O | - | - | +| 指派/改派 | - | - | - | O | - | - | +| 退款审核 | - | - | - | - | O | - | +| 运营看板 | - | - | - | - | - | O | +| 区域数据 | - | - | - | - | - | O | +| 角色切换 | O | O | O | O | O | O | + +--- + +## 七、权限配置检查清单 + +部署后验证以下内容: + +- [ ] 各角色能登录对应端(管理后台/小程序/App) +- [ ] 管理后台菜单根据角色正确显示/隐藏 +- [ ] API `@PreAuthorize` 权限校验生效(无权限返回 403) +- [ ] 数据权限隔离: 运营经理只看本区域, 陪诊师只看自己订单 +- [ ] 小程序 TabBar 根据 user_type 正确渲染 +- [ ] 角色切换后功能权限同步更新 diff --git a/docs/配置项说明.md b/docs/配置项说明.md new file mode 100644 index 0000000..3587d5f --- /dev/null +++ b/docs/配置项说明.md @@ -0,0 +1,190 @@ +# 瑞来健康 — 配置项说明 + +> 更新日期: 2026-06-08 +> 适用于: 生产环境部署 + +--- + +## 一、环境变量 (.env) + +部署时通过 `docker-compose.yml` 从 `.env` 文件注入环境变量。文件位置: + +``` +/home/renjianbo/saars/rlz/.env +``` + +### 1.1 基础运行配置 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `BACKEND_PUBLISH` | 8039 | 后端服务暴露端口 | +| `SERVER_PORT` | 8039 | Spring Boot 监听端口 | +| `SPRING_PROFILES_ACTIVE` | docker | 激活的 Spring Profile | +| `RUOYI_PROFILE` | /data/ruoyi/upload | 文件上传目录 | + +### 1.2 数据库 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `MYSQL_HOST` | (必填) | MySQL 主机地址 | +| `MYSQL_PORT` | 3306 | MySQL 端口 | +| `MYSQL_DATABASE` | rlz | 数据库名 | +| `MYSQL_USER` | root | 数据库用户 | +| `MYSQL_PASSWORD` | (必填) | 数据库密码 | +| `MYSQL_USE_SSL` | true | 是否使用 SSL 连接 | + +当前生产环境使用 **腾讯云 CynosDB MySQL**: +- 地址: `gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com:24936` + +### 1.3 Redis + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `REDIS_HOST` | redis | Redis 主机(docker-compose 内部) | +| `REDIS_PORT` | 6379 | Redis 端口 | + +### 1.4 安全配置 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `TOKEN_SECRET` | (必填) | JWT Token 签名密钥, 建议 `openssl rand -hex 32` 生成 | +| `SWAGGER_ENABLED` | false | Swagger API 文档开关(生产环境必须 false) | +| `DRUID_MONITOR_ENABLED` | false | Druid 监控面板开关(生产环境必须 false) | +| `DRUID_ALLOW_IP` | 127.0.0.1 | Druid 允许访问 IP | +| `DRUID_PASSWORD` | (空) | Druid 监控登录密码 | +| `AES_ENCRYPT_KEY` | baiyangdianzicom | AES 加密密钥(用于 authToken 生成) | + +### 1.5 微信支付配置 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `WX_APP_ID` | (必填) | 微信小程序 AppID | +| `WX_SECRET` | (必填) | 微信小程序 AppSecret | +| `WX_MCH_ID` | (必填) | 微信支付商户号 | +| `WX_MCH_SERIAL_NO` | (必填) | 商户 API 证书序列号 | +| `WX_API_V3_KEY` | (必填) | API v3 密钥(32位) | +| `WX_PRIVATE_KEY` | (必填) | 商户 RSA 私钥(PKCS#8 PEM格式) | +| `WX_NOTIFY_URL` | (必填) | 支付回调地址 | +| `WX_REFUND_NOTIFY_URL` | (必填) | 退款回调地址 | + +**注意**: `WX_PRIVATE_KEY` 为多行 PEM 格式, 直接写入 `.env` 需要特殊处理: +```bash +# 方案一: base64 编码后写入(推荐) +WX_PRIVATE_KEY_BASE64=$(cat apiclient_key.pem | base64 -w0) + +# 方案二: 使用 Docker secrets +``` + +--- + +## 二、application.yml 主要配置项 + +文件位置: `rlz/ruoyi-admin/src/main/resources/application.yml` + +### 2.1 Token 配置 + +```yaml +token: + header: Authorization # JWT 请求头 + secret: ${TOKEN_SECRET} # 签名密钥(来自环境变量) + expireTime: 30 # 过期时间(天) +``` + +### 2.2 验证码配置 + +```yaml +captcha: + type: math # 验证码类型: math(数学) / char(字符) + expiration: 2 # Redis 中验证码有效期(分钟) +``` + +### 2.3 文件上传 + +```yaml +ruoyi: + profile: /data/ruoyi/upload # 上传文件存储路径 + uploadPath: /rlz # 对外访问路径前缀 +``` + +--- + +## 三、推广分润配置 + +通过 `rlz_promoter_config` 表管理, 支持运行时动态调整: + +| config_key | 默认值 | 说明 | +|------------|--------|------| +| `level1_rate` | 5 | 一级推广佣金比例(%) | +| `level2_rate` | 3 | 二级推广佣金比例(%) | +| `level3_rate` | 2 | 三级推广佣金比例(%) | +| `caregiver_rate` | 80 | 陪诊师服务费比例(%) | +| `dispatcher_rate` | 2 | 调度员分润比例(%) | +| `operations_rate` | 1 | 运营经理区域分润比例(%) | + +**修改方式**: 登录管理后台 → 财务管理 → 平台配置, 或直接 SQL: +```sql +UPDATE rlz_promoter_config SET config_value = '6' WHERE config_key = 'level1_rate'; +``` + +### 分润计算示例 + +订单金额 1000 元: +| 角色 | 比例 | 金额 | +|------|------|------| +| 陪诊师 | 80% | 800 元 | +| 调度员 | 2% | 20 元 | +| 一级推广员 | 5% | 50 元 | +| 二级推广员 | 3% | 30 元 | +| 三级推广员 | 2% | 20 元 | +| 运营经理 | 1% | 10 元 | +| 平台留存 | 7% | 70 元 | + +--- + +## 四、Nginx 配置 + +管理后台和 API 通过 Nginx 反向代理。当前配置容器: `rlz-ui-server` (端口 8050) + +关键配置片段: +```nginx +# 管理后台 +location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; +} + +# 后端 API 代理(通过域名 prod-api 转发) +location /prod-api/ { + proxy_pass http://rlz-backend:8039/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; +} +``` + +对外访问地址: +- 管理后台: `http://101.43.95.130:8050` +- 后端 API: `https://ruilaizipj.com/prod-api/` (Nginx HTTPS) + +--- + +## 五、健康检查 + +```bash +# 后端健康检查 +curl -s http://localhost:8039/captchaImage | head -c 100 + +# Redis +docker exec rlz-redis redis-cli PING + +# 查看容器状态 +docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +``` + +## 六、日志位置 + +| 日志 | 路径 | +|------|------| +| 后端应用日志 | `/data/ruoyi/logs/` | +| Docker 容器日志 | `docker logs rlz-backend` | +| 操作日志 | `sys_oper_log` 表 | +| 登录日志 | `sys_logininfor` 表 | diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/main/activity/MainActivity.java b/peizhen/app/src/main/java/com/ruilaizi/service/main/activity/MainActivity.java index cf86d6b..545f749 100644 --- a/peizhen/app/src/main/java/com/ruilaizi/service/main/activity/MainActivity.java +++ b/peizhen/app/src/main/java/com/ruilaizi/service/main/activity/MainActivity.java @@ -25,83 +25,181 @@ import com.ruilaizi.service.utils.checkVersionsUtils.ProfileSpUtils; public class MainActivity extends BaseActivity implements View.OnClickListener { private FragmentManager fm; - private HomeFragment mHomeFragment; - private TraceFragment traceFragment; - private MyFragment mMineFragment; + private Fragment mHomeFragment; // Tab1: role-specific + private Fragment traceFragment; // Tab2: order management + private Fragment mMineFragment; // Tab3: profile private Fragment mCurrent; private ActivityMainBinding bingding; + private String userType = "B"; // default caregiver + + // Tab labels per role + private String tab1Label, tab2Label, tab3Label; + private int tab1Icon, tab1IconActive, tab2Icon, tab2IconActive, tab3Icon, tab3IconActive; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); bingding = DataBindingUtil.setContentView(this, R.layout.activity_main); + + // Read user role from stored profile + readUserType(); + configureTabs(); + initView(); - //给个初始的个人信息,防止其他地方调用会出现空指针 - boolean b= ProfileSpUtils.getInstance().isLogin(); - Log.e("是否登陆",b+""); + // Ensure login bean exists for null safety + boolean b = ProfileSpUtils.getInstance().isLogin(); + Log.e("是否登陆", b + ""); if (!ProfileSpUtils.getInstance().isLogin()) { loginBean loginBean = new loginBean(); - com.ruilaizi.service.main.my.entity.loginBean.DataBean dataBean = new loginBean.DataBean(); + loginBean.DataBean dataBean = new loginBean.DataBean(); dataBean.setNickName("123"); loginBean.setData(dataBean); ProfileSpUtils.getInstance().saveProfile(loginBean); } - //设置公共参数 + // Set common HTTP headers loginBean userProfie = ProfileSpUtils.getInstance().getUserProfie(); if (userProfie.getData() != null) { String token = userProfie.getData().getToken(); HttpHeaders headers = new HttpHeaders(); - headers.put("Authorization", "Bearer " + token); //header不支持中文,不允许有特殊字符 + headers.put("Authorization", "Bearer " + token); OkGo.getInstance().addCommonHeaders(headers); } initFragment(); } + /** + * Read userType from stored login profile + */ + private void readUserType() { + try { + loginBean profile = ProfileSpUtils.getInstance().getUserProfie(); + if (profile != null && profile.getData() != null && profile.getData().getUserType() != null) { + userType = profile.getData().getUserType(); + } + } catch (Exception e) { + Log.e("MainActivity", "Failed to read userType", e); + } + Log.e("MainActivity", "userType=" + userType); + } + + /** + * Configure tab labels, icons based on role + */ + private void configureTabs() { + switch (userType) { + case "05": // 调度员 + tab1Label = "待派单"; + tab2Label = "全部订单"; + tab3Label = "我的"; + tab1Icon = R.mipmap.icon_task; + tab1IconActive = R.mipmap.icon_task; + tab2Icon = R.mipmap.icon_kehuhui; + tab2IconActive = R.mipmap.icon_kehu; + tab3Icon = R.mipmap.icon_wodehui; + tab3IconActive = R.mipmap.icon_wode; + break; + case "04": // 客服 + tab1Label = "退款审核"; + tab2Label = "全部订单"; + tab3Label = "我的"; + tab1Icon = R.mipmap.icon_task; + tab1IconActive = R.mipmap.icon_task; + tab2Icon = R.mipmap.icon_kehuhui; + tab2IconActive = R.mipmap.icon_kehu; + tab3Icon = R.mipmap.icon_wodehui; + tab3IconActive = R.mipmap.icon_wode; + break; + case "03": // 运营经理 + tab1Label = "运营看板"; + tab2Label = "全部订单"; + tab3Label = "我的"; + tab1Icon = R.mipmap.icon_task; + tab1IconActive = R.mipmap.icon_task; + tab2Icon = R.mipmap.icon_kehuhui; + tab2IconActive = R.mipmap.icon_kehu; + tab3Icon = R.mipmap.icon_wodehui; + tab3IconActive = R.mipmap.icon_wode; + break; + default: // B 陪诊师 + tab1Label = "首页"; + tab2Label = "客户"; + tab3Label = "我的"; + tab1Icon = R.mipmap.icon_task; + tab1IconActive = R.mipmap.icon_task; + tab2Icon = R.mipmap.icon_kehuhui; + tab2IconActive = R.mipmap.icon_kehu; + tab3Icon = R.mipmap.icon_wodehui; + tab3IconActive = R.mipmap.icon_wode; + break; + } + } + private void initView() { + // Set dynamic tab labels + bingding.tvHome.setText(tab1Label); + bingding.tvMap.setText(tab2Label); + bingding.tvMine.setText(tab3Label); + bingding.rlHome.setOnClickListener(this); bingding.rlMap.setOnClickListener(this); bingding.rlMine.setOnClickListener(this); - bingding.tvHomeImg.setBackgroundResource(R.mipmap.icon_task); + bingding.tvHomeImg.setBackgroundResource(tab1IconActive); bingding.tvHome.setTextColor(getResources().getColor(R.color.colortheme)); } private void initFragment() { - StatusBarUtil.transparencyBar(this); //设置状态栏全透明 + StatusBarUtil.transparencyBar(this); StatusBarUtil.setStatusBarColor(this, R.color.colortheme); - mHomeFragment = new HomeFragment(); + mHomeFragment = createTab1Fragment(); fm = getSupportFragmentManager(); - fm.beginTransaction(); FragmentTransaction fragmentTransaction = fm.beginTransaction(); fragmentTransaction.replace(R.id.content_layout, mHomeFragment); fragmentTransaction.commit(); } + /** + * Create role-specific fragment for Tab1 + */ + private Fragment createTab1Fragment() { + switch (userType) { + case "05": + return new com.ruilaizi.service.main.kehu.fragment.DispatchFragment(); + case "04": + return new com.ruilaizi.service.main.kehu.fragment.RefundFragment(); + case "03": + return new com.ruilaizi.service.main.kehu.fragment.OpsFragment(); + default: + return new HomeFragment(); + } + } + @Override public void onClick(View v) { FragmentTransaction fragmentTransaction = fm.beginTransaction(); int id = v.getId(); if (id == R.id.rl_home) { - bingding.tvHomeImg.setBackgroundResource(R.mipmap.icon_task); - bingding.tvMapImg.setBackgroundResource(R.mipmap.icon_kehuhui); - bingding.tvMineImg.setBackgroundResource(R.mipmap.icon_wodehui); + bingding.tvHomeImg.setBackgroundResource(tab1IconActive); + bingding.tvMapImg.setBackgroundResource(tab2Icon); + bingding.tvMineImg.setBackgroundResource(tab3Icon); bingding.tvHome.setTextColor(getResources().getColor(R.color.colortheme)); bingding.tvMap.setTextColor(getResources().getColor(R.color.colorhui)); bingding.tvMine.setTextColor(getResources().getColor(R.color.colorhui)); hideFragment(traceFragment, fragmentTransaction); hideFragment(mMineFragment, fragmentTransaction); if (mHomeFragment == null) { + mHomeFragment = createTab1Fragment(); fragmentTransaction.add(R.id.content_layout, mHomeFragment); } else { mCurrent = mHomeFragment; fragmentTransaction.show(mHomeFragment); } broadcastIntent(); - StatusBarUtil.transparencyBar(this); //设置状态栏全透明 + StatusBarUtil.transparencyBar(this); StatusBarUtil.setStatusBarColor(this, R.color.colortheme); } else if (id == R.id.rl_map) { - bingding.tvHomeImg.setBackgroundResource(R.mipmap.icon_task_hui); - bingding.tvMapImg.setBackgroundResource(R.mipmap.icon_kehu); - bingding.tvMineImg.setBackgroundResource(R.mipmap.icon_wodehui); + bingding.tvHomeImg.setBackgroundResource(tab1Icon); + bingding.tvMapImg.setBackgroundResource(tab2IconActive); + bingding.tvMineImg.setBackgroundResource(tab3Icon); bingding.tvHome.setTextColor(getResources().getColor(R.color.colorhui)); bingding.tvMap.setTextColor(getResources().getColor(R.color.colortheme)); bingding.tvMine.setTextColor(getResources().getColor(R.color.colorhui)); @@ -115,13 +213,13 @@ public class MainActivity extends BaseActivity implements View.OnClickListener { mCurrent = traceFragment; fragmentTransaction.show(traceFragment); } - StatusBarUtil.transparencyBar(this); //设置状态栏全透明 - StatusBarUtil.StatusBarLightMode(this); //设置白底黑字 + StatusBarUtil.transparencyBar(this); + StatusBarUtil.StatusBarLightMode(this); StatusBarUtil.setStatusBarColor(this, R.color.colorPrimary); } else if (id == R.id.rl_mine) { - bingding.tvHomeImg.setBackgroundResource(R.mipmap.icon_task_hui); - bingding.tvMapImg.setBackgroundResource(R.mipmap.icon_kehuhui); - bingding.tvMineImg.setBackgroundResource(R.mipmap.icon_wode); + bingding.tvHomeImg.setBackgroundResource(tab1Icon); + bingding.tvMapImg.setBackgroundResource(tab2Icon); + bingding.tvMineImg.setBackgroundResource(tab3IconActive); bingding.tvHome.setTextColor(getResources().getColor(R.color.colorhui)); bingding.tvMap.setTextColor(getResources().getColor(R.color.colorhui)); bingding.tvMine.setTextColor(getResources().getColor(R.color.colortheme)); @@ -134,7 +232,7 @@ public class MainActivity extends BaseActivity implements View.OnClickListener { mCurrent = mMineFragment; fragmentTransaction.show(mMineFragment); } - StatusBarUtil.transparencyBar(this); //设置状态栏全透明 + StatusBarUtil.transparencyBar(this); StatusBarUtil.setStatusBarColor(this, R.color.colortheme); } fragmentTransaction.commit(); diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/DispatchFragment.java b/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/DispatchFragment.java new file mode 100644 index 0000000..3bcc8ef --- /dev/null +++ b/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/DispatchFragment.java @@ -0,0 +1,226 @@ +package com.ruilaizi.service.main.kehu.fragment; + +import android.app.Dialog; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.BaseViewHolder; +import com.lzy.okgo.OkGo; +import com.lzy.okgo.cache.CacheMode; +import com.lzy.okgo.convert.StringConvert; +import com.lzy.okgo.model.Response; +import com.lzy.okrx2.adapter.ObservableResponse; +import com.ruilaizi.service.R; +import com.ruilaizi.service.base.BaseFragment; +import com.ruilaizi.service.okgonet.HttpConstants; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * DispatchFragment — 调度员:待派单订单列表 + 指派陪诊师 + */ +public class DispatchFragment extends BaseFragment { + + private RecyclerView recyclerView; + private OrderAdapter adapter; + private List orderList = new ArrayList<>(); + private List caregiverList = new ArrayList<>(); + private Dialog assignDialog; + private OrderItem selectedOrder; + private int selectedCaregiverIdx = -1; + + // --- Data classes --- + static class OrderItem { + String orderId, orderNumber, orderMoney, yuguMoney, status; + String createTime, hospitalName, serviceName, cId; + } + static class CaregiverItem { + String userId, nickName, phonenumber; + } + + // --- Adapter --- + class OrderAdapter extends BaseQuickAdapter { + OrderAdapter(List data) { + super(R.layout.item_dispatch_order, data); + } + @Override + protected void convert(BaseViewHolder h, OrderItem item) { + h.setText(R.id.tv_order_no, item.orderNumber != null ? item.orderNumber : "#" + item.orderId); + h.setText(R.id.tv_amount, "¥" + (item.yuguMoney != null ? item.yuguMoney : item.orderMoney)); + h.setText(R.id.tv_info, (item.hospitalName != null ? item.hospitalName : "") + " | " + (item.serviceName != null ? item.serviceName : "")); + h.setText(R.id.tv_time, item.createTime); + h.getView(R.id.btn_action).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showAssignDialog(item); + } + }); + } + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mContext = getActivity(); + View view = inflater.inflate(R.layout.fragment_dispatch, container, false); + recyclerView = view.findViewById(R.id.recycler_view); + adapter = new OrderAdapter(orderList); + recyclerView.setLayoutManager(new LinearLayoutManager(mContext)); + recyclerView.setAdapter(adapter); + loadPendingOrders(); + return view; + } + + private void loadPendingOrders() { + OkGo.get(HttpConstants.URi_system_dispatch_pending) + .converter(new StringConvert()) + .cacheMode(CacheMode.NO_CACHE) + .adapt(new ObservableResponse()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new io.reactivex.Observer>() { + @Override public void onSubscribe(@NonNull Disposable d) {} + @Override public void onNext(@NonNull Response resp) { + try { + JSONObject json = JSON.parseObject(resp.body()); + if (json.getIntValue("code") == 200) { + JSONArray arr = json.getJSONArray("data"); + orderList.clear(); + if (arr != null) for (int i = 0; i < arr.size(); i++) { + orderList.add(JSON.toJavaObject(arr.getJSONObject(i), OrderItem.class)); + } + adapter.notifyDataSetChanged(); + } + } catch (Exception e) { Log.e("Dispatch", "parse err", e); } + } + @Override public void onError(@NonNull Throwable e) { + Toast.makeText(mContext, "加载失败", Toast.LENGTH_SHORT).show(); + } + @Override public void onComplete() {} + }); + } + + private void loadCaregivers() { + caregiverList.clear(); + OkGo.get(HttpConstants.URi_system_dispatch_caregivers) + .converter(new StringConvert()).cacheMode(CacheMode.NO_CACHE) + .adapt(new ObservableResponse()) + .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + .subscribe(new io.reactivex.Observer>() { + @Override public void onSubscribe(@NonNull Disposable d) {} + @Override public void onNext(@NonNull Response resp) { + try { + JSONObject json = JSON.parseObject(resp.body()); + if (json.getIntValue("code") == 200) { + JSONArray arr = json.getJSONArray("data"); + if (arr != null) for (int i = 0; i < arr.size(); i++) { + caregiverList.add(JSON.toJavaObject(arr.getJSONObject(i), CaregiverItem.class)); + } + } + } catch (Exception e) { Log.e("Dispatch", "cg err", e); } + } + @Override public void onError(@NonNull Throwable e) {} + @Override public void onComplete() {} + }); + } + + private void showAssignDialog(OrderItem order) { + selectedOrder = order; + selectedCaregiverIdx = -1; + loadCaregivers(); + + assignDialog = new Dialog(mContext, R.style.DialogTheme); + View view = LayoutInflater.from(mContext).inflate(R.layout.dialog_assign, null); + assignDialog.setContentView(view); + + Window window = assignDialog.getWindow(); + window.setGravity(android.view.Gravity.BOTTOM); + window.setWindowAnimations(R.style.dialog_menu_animStyle); + window.getDecorView().setPadding(0, 0, 0, 0); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + window.setAttributes(lp); + + TextView tvNo = view.findViewById(R.id.tv_order_no); + tvNo.setText("订单: " + (order.orderNumber != null ? order.orderNumber : "#" + order.orderId)); + + ListView listView = view.findViewById(R.id.list_caregivers); + List names = new ArrayList<>(); + for (CaregiverItem c : caregiverList) { + names.add((c.nickName != null ? c.nickName : "") + " (" + (c.phonenumber != null ? c.phonenumber : "") + ")"); + } + android.widget.ArrayAdapter la = new android.widget.ArrayAdapter<>(mContext, + android.R.layout.simple_list_item_single_choice, names); + listView.setAdapter(la); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override public void onItemClick(AdapterView p, View v, int pos, long id) { selectedCaregiverIdx = pos; } + }); + + view.findViewById(R.id.btn_confirm).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + if (selectedCaregiverIdx >= 0 && selectedCaregiverIdx < caregiverList.size()) { + doAssign(caregiverList.get(selectedCaregiverIdx).userId); + } else { + Toast.makeText(mContext, "请选择陪诊师", Toast.LENGTH_SHORT).show(); + } + } + }); + view.findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { assignDialog.dismiss(); } + }); + + assignDialog.show(); + } + + private void doAssign(String caregiverId) { + OkGo.post(HttpConstants.URi_system_dispatch_assign + selectedOrder.orderId + "/" + caregiverId) + .converter(new StringConvert()) + .adapt(new ObservableResponse()) + .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + .subscribe(new io.reactivex.Observer>() { + @Override public void onSubscribe(@NonNull Disposable d) {} + @Override public void onNext(@NonNull Response resp) { + try { + JSONObject json = JSON.parseObject(resp.body()); + if (json.getIntValue("code") == 200) { + Toast.makeText(mContext, "派单成功", Toast.LENGTH_SHORT).show(); + assignDialog.dismiss(); + loadPendingOrders(); + } else { + Toast.makeText(mContext, json.getString("msg"), Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) {} + } + @Override public void onError(@NonNull Throwable e) { + Toast.makeText(mContext, "派单失败", Toast.LENGTH_SHORT).show(); + } + @Override public void onComplete() {} + }); + } +} diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/OpsFragment.java b/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/OpsFragment.java new file mode 100644 index 0000000..377abc1 --- /dev/null +++ b/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/OpsFragment.java @@ -0,0 +1,45 @@ +package com.ruilaizi.service.main.kehu.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.Nullable; + +import com.ruilaizi.service.R; +import com.ruilaizi.service.base.BaseFragment; +import com.ruilaizi.service.okgonet.HttpConstants; + +/** + * OpsFragment — 运营经理:WebView 加载后台运营看板 + */ +public class OpsFragment extends BaseFragment { + + private WebView webView; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mContext = getActivity(); + View view = inflater.inflate(R.layout.fragment_ops, container, false); + + webView = view.findViewById(R.id.web_view); + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setUseWideViewPort(true); + settings.setLoadWithOverviewMode(true); + + webView.setWebViewClient(new WebViewClient()); + + // Load admin dashboard URL (use the base URL to construct admin panel URL) + String adminUrl = HttpConstants.URiBase.replace("/dev-api", "") + "/index"; + webView.loadUrl(adminUrl); + + return view; + } +} diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/RefundFragment.java b/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/RefundFragment.java new file mode 100644 index 0000000..394da23 --- /dev/null +++ b/peizhen/app/src/main/java/com/ruilaizi/service/main/kehu/fragment/RefundFragment.java @@ -0,0 +1,199 @@ +package com.ruilaizi.service.main.kehu.fragment; + +import android.app.Dialog; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.BaseViewHolder; +import com.lzy.okgo.OkGo; +import com.lzy.okgo.cache.CacheMode; +import com.lzy.okgo.convert.StringConvert; +import com.lzy.okgo.model.Response; +import com.lzy.okrx2.adapter.ObservableResponse; +import com.ruilaizi.service.R; +import com.ruilaizi.service.base.BaseFragment; +import com.ruilaizi.service.okgonet.HttpConstants; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * RefundFragment — 客服专员:退款审核列表 + 通过/驳回 + */ +public class RefundFragment extends BaseFragment { + + private RecyclerView recyclerView; + private RefundAdapter adapter; + private List orderList = new ArrayList<>(); + private Dialog rejectDialog; + private RefundItem selectedItem; + + static class RefundItem { + String orderId, orderNumber, orderMoney, yuguMoney, status; + String createTime, cId, bId, hospitalName; + } + + class RefundAdapter extends BaseQuickAdapter { + RefundAdapter(List data) { + super(R.layout.item_refund_order, data); + } + @Override + protected void convert(BaseViewHolder h, RefundItem item) { + h.setText(R.id.tv_order_no, item.orderNumber != null ? item.orderNumber : "#" + item.orderId); + h.setText(R.id.tv_amount, "¥" + (item.yuguMoney != null ? item.yuguMoney : item.orderMoney)); + h.setText(R.id.tv_info, "患者ID: " + (item.cId != null ? item.cId : "--") + " | 陪诊师ID: " + (item.bId != null ? item.bId : "--")); + h.setText(R.id.tv_time, item.createTime); + h.getView(R.id.btn_approve).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { doApprove(item, true); } + }); + h.getView(R.id.btn_reject).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { showRejectDialog(item); } + }); + } + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mContext = getActivity(); + View view = inflater.inflate(R.layout.fragment_refund, container, false); + recyclerView = view.findViewById(R.id.recycler_view); + adapter = new RefundAdapter(orderList); + recyclerView.setLayoutManager(new LinearLayoutManager(mContext)); + recyclerView.setAdapter(adapter); + loadRefundOrders(); + return view; + } + + private void loadRefundOrders() { + OkGo.get(HttpConstants.URi_system_refund_list) + .converter(new StringConvert()).cacheMode(CacheMode.NO_CACHE) + .adapt(new ObservableResponse()) + .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + .subscribe(new io.reactivex.Observer>() { + @Override public void onSubscribe(@NonNull Disposable d) {} + @Override public void onNext(@NonNull Response resp) { + try { + JSONObject json = JSON.parseObject(resp.body()); + if (json.getIntValue("code") == 200) { + JSONArray arr = json.getJSONArray("rows"); + if (arr == null) arr = json.getJSONArray("data"); + orderList.clear(); + if (arr != null) for (int i = 0; i < arr.size(); i++) { + orderList.add(JSON.toJavaObject(arr.getJSONObject(i), RefundItem.class)); + } + adapter.notifyDataSetChanged(); + } + } catch (Exception e) { Log.e("Refund", "parse err", e); } + } + @Override public void onError(@NonNull Throwable e) { + Toast.makeText(mContext, "加载失败", Toast.LENGTH_SHORT).show(); + } + @Override public void onComplete() {} + }); + } + + private void doApprove(RefundItem item, boolean approve) { + String url = HttpConstants.URi_system_refund_approve + item.orderId; + OkGo.post(url) + .params("approved", approve ? "1" : "0") + .converter(new StringConvert()) + .adapt(new ObservableResponse()) + .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + .subscribe(new io.reactivex.Observer>() { + @Override public void onSubscribe(@NonNull Disposable d) {} + @Override public void onNext(@NonNull Response resp) { + try { + JSONObject json = JSON.parseObject(resp.body()); + if (json.getIntValue("code") == 200) { + Toast.makeText(mContext, approve ? "已通过" : "已驳回", Toast.LENGTH_SHORT).show(); + loadRefundOrders(); + } else { + Toast.makeText(mContext, json.getString("msg"), Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) {} + } + @Override public void onError(@NonNull Throwable e) { + Toast.makeText(mContext, "操作失败", Toast.LENGTH_SHORT).show(); + } + @Override public void onComplete() {} + }); + } + + private void showRejectDialog(RefundItem item) { + selectedItem = item; + rejectDialog = new Dialog(mContext, R.style.DialogTheme); + View view = LayoutInflater.from(mContext).inflate(R.layout.dialog_reject_reason, null); + rejectDialog.setContentView(view); + + Window window = rejectDialog.getWindow(); + window.setGravity(android.view.Gravity.BOTTOM); + window.setWindowAnimations(R.style.dialog_menu_animStyle); + window.getDecorView().setPadding(0, 0, 0, 0); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + window.setAttributes(lp); + + EditText etReason = view.findViewById(R.id.et_reason); + view.findViewById(R.id.btn_submit).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + rejectDialog.dismiss(); + doRejectWithReason(selectedItem, etReason.getText().toString()); + } + }); + view.findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { rejectDialog.dismiss(); } + }); + + rejectDialog.show(); + } + + private void doRejectWithReason(RefundItem item, String reason) { + OkGo.post(HttpConstants.URi_system_refund_approve + item.orderId) + .params("approved", "0") + .params("reason", reason) + .converter(new StringConvert()) + .adapt(new ObservableResponse()) + .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + .subscribe(new io.reactivex.Observer>() { + @Override public void onSubscribe(@NonNull Disposable d) {} + @Override public void onNext(@NonNull Response resp) { + try { + JSONObject json = JSON.parseObject(resp.body()); + if (json.getIntValue("code") == 200) { + Toast.makeText(mContext, "已驳回", Toast.LENGTH_SHORT).show(); + loadRefundOrders(); + } else { + Toast.makeText(mContext, json.getString("msg"), Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) {} + } + @Override public void onError(@NonNull Throwable e) { + Toast.makeText(mContext, "操作失败", Toast.LENGTH_SHORT).show(); + } + @Override public void onComplete() {} + }); + } +} diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/main/my/MyFragment.java b/peizhen/app/src/main/java/com/ruilaizi/service/main/my/MyFragment.java index a87141b..3a870f1 100644 --- a/peizhen/app/src/main/java/com/ruilaizi/service/main/my/MyFragment.java +++ b/peizhen/app/src/main/java/com/ruilaizi/service/main/my/MyFragment.java @@ -76,6 +76,7 @@ public class MyFragment extends BaseTreeFragment + + + + + + + + + + +