feat: add request.js utility, service page, and fix hospital image URLs

- Add request.js for unified API calls with loading/error handling
- Add service page (service type listing)
- Fix hospital list to load from API with proper image URL construction
- Add service types dynamic loading on index page
- Various mini program improvements (order, care, wallet pages)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-06-07 10:13:26 +08:00
parent d0d69387fb
commit 1d0a0b9b04
79 changed files with 3152 additions and 1539 deletions

View File

@@ -1,6 +1,7 @@
{
"pages": [
"pages/index/index",
"pages/service/service",
"pages/peihu/peihu",
"pages/tuiguang/extensionInfor",
"pages/my/my",
@@ -48,6 +49,12 @@
"iconPath": "pages/image/income.png",
"selectedIconPath": "pages/image/income_a.png"
},
{
"pagePath": "pages/service/service",
"text": "服务",
"iconPath": "pages/image/service.png",
"selectedIconPath": "pages/image/service.png"
},
{
"pagePath": "pages/order/order",
"text": "订单",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -1,83 +1,17 @@
//获取应用实例
var app = getApp();
const { request } = require('../../utils/request')
Page({
/**
* 页面的初始数据00
*/
data: {
order_list: [],
hospitalData: [],
bannerdata: [
"home-banner",
"home-banner",
"home-banner"
],
itemView: [{
icon: "全程陪诊",
title: "全程陪诊",
price: "200",
describe: "服务内容:排队 预约 挂号 缴费 取药 记录医嘱",
ordertype: "1"
},
{
icon: "诊前约号",
title: "诊前约号",
price: "20",
describe: "服务内容:协助办理完成挂号",
ordertype: "2"
},
// {
// icon: "代办买药",
// title: "代办买药",
// price:"30",
// describe:"服务内容:协助代理买药"
// },
{
icon: "代办问诊",
title: "代办问诊",
price: "100",
describe: "服务内容:使用病人或者家属取检查报告化验单向医生问明情况",
ordertype: "4"
},
{
icon: "取送结果",
title: "取送结果",
price: "50",
describe: "服务内容:包括给病人或者家属取检查报告化验单等",
ordertype: "3"
},
// {
// icon: "出入院办理",
// title: "出入院办理",
// price:"100",
// describe:"协助办理出入院手续"
// }
],
hospitalData: [{
img: "https://bkimg.cdn.bcebos.com/pic/8326cffc1e178a828fa58d64f303738da877e85d?x-bce-process=image/resize,m_lfit,w_536,limit_1",
name: "西安市高新医院",
grade: "三甲",
genre: "综合医院",
skiled: "消化内科、心血管内科",
address: "高新区高新六路",
id: "1"
}, {
img: "https://bkimg.cdn.bcebos.com/pic/8326cffc1e178a828fa58d64f303738da877e85d?x-bce-process=image/resize,m_lfit,w_536,limit_1",
name: "第四军医大学西京医院",
grade: "三甲",
genre: "综合医院",
skiled: "消化内科、心血管内科",
address: "莲湖区康复路",
id: "2"
}, {
img: "https://bkimg.cdn.bcebos.com/pic/8326cffc1e178a828fa58d64f303738da877e85d?x-bce-process=image/resize,m_lfit,w_536,limit_1",
name: "陕西省人民医院",
grade: "三甲",
genre: "综合医院",
skiled: "消化内科、心血管内科",
address: "碑林区友谊西路",
id: "3"
}]
itemView: [],
firstLoad: true
},
/**
@@ -101,6 +35,10 @@ Page({
}
},
goService() {
wx.switchTab({ url: '/pages/service/service' })
},
go_peizhen() {
if (wx.getStorageSync('phone') == '') {
wx.setStorageSync('uidFlag', false);
@@ -124,33 +62,60 @@ Page({
})
}
},
//获取服务类型列表(从后台动态加载,最多显示8个热门推荐)
getServiceTypes: function () {
var that = this;
request({
url: '/system/serviceType/enabled',
method: 'GET',
data: { category: '2' },
loading: false
}).then(res => {
var services = res.data || [];
if (services.length) {
var mapped = services.slice(0, 8).map(function (s) {
return {
icon: s.icon || s.name,
title: s.name,
price: s.price,
describe: s.description || '',
ordertype: s.typeCode
}
})
that.setData({ itemView: mapped })
}
}).catch(() => {})
},
//获取医院列表
getHospitalList: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/hospital/list',
header: app.getRequestHeader(),
request({
url: '/system/hospital/list',
method: 'GET',
data: {},
success: (res) => {
var resp = res.data;
console.log("resp==" + resp)
if (res.data.code == 200) {
if (this.data.pageNum == 1) {
that.setData({
order_list: resp.data,
pageNum: this.data.pageNum + 1
});
} else {
that.setData({
order_list: resp.data,
});
loading: false
}).then(res => {
var hospitals = res.rows || res.data || [];
if (hospitals.length) {
var baseUrl = app.globalData.url;
var mapped = hospitals.map(function (h) {
var imgUrl = h.hospitalImage || '';
if (imgUrl && imgUrl.indexOf('http') !== 0) {
imgUrl = baseUrl + imgUrl;
}
return {
id: h.id,
name: h.hospitalname,
grade: h.hospitalLevel || '三甲',
genre: h.hospitalType || '综合医院',
skiled: h.hospitalDesc || '',
address: h.address || '',
img: imgUrl
}
})
that.setData({ hospitalData: mapped })
}
});
}).catch(() => {})
},
/**
* 医院列表
@@ -205,28 +170,17 @@ Page({
* 生命周期函数--监听页面显示
*/
onShow() {
if (this.data.firstLoad) {
this.setData({ firstLoad: false })
this.getServiceTypes()
this.getHospitalList()
}
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.getServiceTypes()
this.getHospitalList()
wx.stopPullDownRefresh()
},
/**

View File

@@ -12,11 +12,15 @@
</view>
</swiper>
</view>
<view class="section-header">
<view class="section-title">热门服务</view>
<view class="section-more" bindtap="goService">查看更多 ></view>
</view>
<view class="item-view">
<block wx:for="{{itemView}}" wx:key="id">
<view class="item-list" bindtap="itemClick" data-id="{{item.title}}" data-item="{{item}}">
<view class="item-image">
<image src="../image/home/{{item.icon}}.png" mode="aspectFit"></image>
<image src="{{item.icon.indexOf('http') === 0 ? item.icon : '../image/home/' + item.icon + '.png'}}" mode="aspectFit"></image>
</view>
<view class="item-title">
<text>{{item.title}}</text>
@@ -52,7 +56,7 @@
<block wx:for="{{hospitalData}}" wx:key="id">
<view class="list-item" bindtap="listItemTap" data-id="{{item.id}}">
<view class="img-back">
<image src="{{item.img}}" mode="aspectFill"></image>
<image src="{{item.img || '/pages/image/empty.png'}}" mode="aspectFill"></image>
</view>
<view class="list-info">
<view class="name"><text>{{item.name}}</text></view>

View File

@@ -40,7 +40,8 @@
.item-view{
display: flex;
flex-direction: row;
margin: 30rpx 40rpx 0;
flex-wrap: wrap;
margin: 20rpx 30rpx 0;
}
#block{
@@ -50,8 +51,8 @@
.item-list{
display: flex;
flex-direction: column;
padding: 0 10rpx;
width: 160rpx;
padding: 0 8rpx;
width: calc(25% - 16rpx);
height: 160rpx;
align-items: center;
justify-content: center;
@@ -208,3 +209,21 @@
font-size: 30rpx;
color: #666666;
}
.section-header{
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 40rpx 0 40rpx;
}
.section-title{
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.section-more{
font-size: 24rpx;
color: #999;
}

View File

@@ -1,207 +1,95 @@
// pages/mechanism/mechanism.js
//获取应用实例
const app = getApp()
const { request } = require('../../utils/request')
Page({
/**
* 页面的初始数据
*/
data: {
navHeight: '',
menuButtonInfo: {},
searchMarginTop: 0, // 搜索框上边距
searchWidth: 0, // 搜索框宽度
searchHeight: 0, // 搜索框高度
page:1,
search: ''
searchMarginTop: 0,
searchWidth: 0,
searchHeight: 0,
page: 1,
search: '',
lists: [],
firstLoad: true
},
/**
* 监听搜索内容
*/
changeSearch: function(e) {
let that = this;
var inputSearch = e.detail.value;
that.setData({
search: inputSearch
})
app.console(inputSearch+"================="+'22dddddddddddddddddddddd2')
// this.dataRefresh()
this.getMechanismList();
changeSearch: function (e) {
this.setData({ search: e.detail.value })
this.getMechanismList()
},
back(){
back() {
wx.navigateBack()
},
//机构详情跳转
mechanism(e){
app.console("================="+'dddddddddddddddd')
mechanism(e) {
wx.navigateTo({
url: "/pages/institutions/institutions?id=" + e.currentTarget.dataset.id,
})
},
Seemore(){
console.log('点击了查看更多');
Seemore() {
console.log('点击了查看更多')
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
this.setData({
menuButtonInfo: wx.getMenuButtonBoundingClientRect()
})
console.log(this.data.menuButtonInfo)
this.setData({ menuButtonInfo: wx.getMenuButtonBoundingClientRect() })
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) ),
searchMarginTop: statusBarHeight + margin, // 状态栏 + 胶囊按钮边距
searchHeight: height, // 与胶囊按钮同高
searchWidth: right - width // 胶囊按钮右边坐标 - 胶囊按钮宽度 = 按钮左边可使用宽度
navHeight: (height + statusBarHeight + (margin * 2)),
searchMarginTop: statusBarHeight + margin,
searchHeight: height,
searchWidth: right - width
})
},
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
this.getMechanismList();
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
//this.getOrderList();
var that = this;
wx.request({
url: app.buildUrl("/app/Goddesswechat/getOrderList"),
header: app.getRequestHeader(),
method:'POST',
data: {
version_number: app.globalData.version_number,
uid:wx.getStorageSync('uid'),
token:wx.getStorageSync('token'),
page:1,
page_size:10,
style: that.data.status[ that.data.currentType ]
},
success: (res)=> {
var resp = res.data;
if(resp.erro==0){
that.setData({
page:1,
order_list:resp.order,
});
wx.stopPullDownRefresh({
success: (res) => {},
})
if (this.data.firstLoad) {
this.setData({ firstLoad: false })
this.getMechanismList()
}
}
});
},
/**
* 页面上拉触底事件的处理函数
*/
onPullDownRefresh: function () {
this.setData({ page: 1 })
this.getMechanismList().finally(() => wx.stopPullDownRefresh())
},
onReachBottom: function () {
this.getMechanismList();
this.getMechanismList()
},
//机构列表
getMechanismList: function () {
var that = this;
wx.request({
url: app.buildUrl("/app/Goddesswechat/getMechanismList"),
header: app.getRequestHeader(),
method:'POST',
var that = this
return request({
url: '/app/Goddesswechat/getMechanismList',
method: 'POST',
loading: true,
data: {
version_number: app.globalData.version_number,
uid:wx.getStorageSync('uid'),
token:wx.getStorageSync('token'),
page:'1',
page_size:500,
city:'',
style_id:'',
serach_name:this.data.search,
px_type:'',
lng:'',
lat:'',
uid: wx.getStorageSync('uid'),
token: wx.getStorageSync('token'),
page: '1',
page_size: 50,
city: '',
style_id: '',
serach_name: this.data.search,
px_type: '',
lng: '',
lat: '',
}
}).then(res => {
if (res.erro === 0) {
that.setData({ lists: res.lists || [] })
}
}).catch(() => {})
},
success: function (res) {
var resp = res.data;
if(resp.erro==0){
that.setData({
lists:resp.lists
});
// if(that.data.page==1){
// that.setData({
// lists:resp.lists,
// page:that.data.page+1
// });
// }else{
// that.setData({
// lists:that.data.lists.concat(resp.lists),
// page:that.data.page+1
// });
// }
}
}
});
},
})

View File

@@ -20,12 +20,9 @@
<view class="order-list" style="margin-top: 160rpx;" wx:if="{{lists.length}}">
<view wx:for="{{lists}}" wx:key="{{index}}" wx:for-item="item">
<view class="recommend" bindtap="mechanism" data-id="{{item.id}}">
<view class="red-image">
<image
src="{{item.wechat_logo}}">
</image>
<image src="{{item.wechat_logo}}"></image>
</view>
<view class="red-tit">
<view style="font-size: 30rpx;color: #333333;font-weight:700">{{item.mechanism_name}}</view>
@@ -40,25 +37,12 @@
<text>已预约{{item.appointment}}</text>
</view>
</view>
</view>
<!-- <view style="margin-left:150rpx">
<view class="headline">
<text>抢</text>
<text style="font-size: 18rpx;font-family: PingFang SC;margin-left:18rpx;">【除皱瘦脸】瘦脸针·国产瘦脸加强版80-正...</text>
<text style="font-size: 18rpx;color:#FF4A4A;margin-left:117rpx;">¥99</text>
</view>
<view class="headline">
<text>抢</text>
<text style="font-size: 18rpx;font-family: PingFang SC;margin-left:18rpx;">【除皱瘦脸】瘦脸针·国产瘦脸加强版80-正...</text>
<text style="font-size: 18rpx;color:#FF4A4A;margin-left:117rpx;">¥99</text>
</view>
</view>
<view class="Residuals" bindtap="Seemore">
查看其他3个相关产品
<view class="jiantou"></view>
</view> -->
</view>
<view wx:else style="display:flex;flex-direction:column;align-items:center;padding-top:300rpx;">
<image style="width:200rpx;height:200rpx;" src="/pages/image/empty.png"></image>
<view style="color:#999;font-size:28rpx;margin-top:20rpx;">暂无机构</view>
</view>

View File

@@ -1,47 +1,32 @@
var Token = require('../../utils/util.js');
const app = getApp()
const { request } = require('../../utils/request')
Page({
/**
* 页面的初始数据
*/
data: {
canIUseOpenData: wx.canIUse('open-data.type.userAvatarUrl') && wx.canIUse('open-data.type.userNickName'),
phone: wx.getStorageSync('phone'),
uidFlag: wx.getStorageSync('uidFlag'),
status: '',
code: '',
height: ''
height: '',
logging: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var statusBarHeight = wx.getSystemInfoSync().statusBarHeight;
app.console(statusBarHeight + "=================xxxxxxxxxxxxxxxxxxxxxxxx")
this.setData({
statusBarHeight: statusBarHeight,
height: 46 + statusBarHeight,
});
app.console(this.data.height + "=================xxxxxxxxxxxxxxxxxxxxxxxx")
//调试参数
// wx.setStorageSync('phone', 18133922183);
// wx.setStorageSync('uid', 'adfdffb45a364fc23162f1ebfe9dd66f');
// wx.setStorageSync('token', '797d4q7XT8/PM9bU+T+CNpEpNfsWeVb5j+eXnrRXW0yq+wKXY9fBNSvb7qPiCRMlzZmwG3u1ods8R8Rh3gITxCkyiZGUtLZf');
// wx.setStorageSync('uidFlag', true);
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
// 仅在未登录时获取 code
if (!this.data.uidFlag) {
this.getLogin()
console.log("code==" + this.data.code)
// if (wx.getStorageSync('phone') != '') {
// this.getUserBaseInfo()
// }
}
},
// 登录
@@ -49,74 +34,62 @@ Page({
var that = this;
wx.login({
success: res => {
console.log(res)
app.globalData.code = res.code
that.setData({
code: res.code
})
that.setData({ code: res.code })
}
})
},
// 获取电话号码
getPhoneNumber(e) {
// 获取电话号码并登录
async getPhoneNumber(e) {
if (this.data.logging) return
this.setData({ logging: true })
console.log("encryptedData==" + e.detail.encryptedData)
console.log("iv==" + e.detail.iv)
console.log("wxcode==" + app.globalData.code)
var _this = this;
console.log(e)
wx.request({
url: app.globalData.url + '/weixinLogin',
try {
var res = await request({
url: '/weixinLogin',
method: 'POST',
loading: true,
loadingText: '登录中...',
data: {
encryptedData: e.detail.encryptedData,
iv: e.detail.iv,
wxcode: app.globalData.code
},
method: 'POST',
header: {
'content-type': 'application/x-www-form-urlencoded'
},
success(res) {
if (res.data.code == 200) {
app.globalData.phone = res.data.phone;
wx.setStorageSync('phone', res.data.data.phoneNumber);
wx.setStorageSync('uid', res.data.data.userId);
wx.setStorageSync('token', res.data.data.token);
}
})
app.globalData.phone = res.phone;
wx.setStorageSync('phone', res.data.phoneNumber);
wx.setStorageSync('uid', res.data.userId);
wx.setStorageSync('token', res.data.token);
wx.setStorageSync('uidFlag', true);
_this.setData({
this.setData({
uidFlag: true,
phone: res.data.phone
phone: res.phone
})
app.globalData.uid = res.data.uid;
wx.switchTab({
url: '/pages/index/index'
})
// Token.token()
_this.getToken()
app.globalData.uid = res.data.userId;
_this.ceshi()
wx.showToast({
title: res.data.msg,
icon: 'none'
});
wx.showToast({ title: '登录成功', icon: 'success' })
// 同步登录信息到服务端(失败不影响登录)
this.syncLogin()
// 返回上一页,若无法返回则到首页
var pages = getCurrentPages()
if (pages.length > 1) {
wx.navigateBack()
} else {
wx.showToast({
title: res.data.msg,
icon: 'none'
});
wx.switchTab({ url: '/pages/index/index' })
}
} catch (e) {
this.setData({ logging: false })
}
}
})
},
//上传测试数据
ceshi() {
// 同步登录信息
syncLogin() {
wx.request({
url: app.buildUrltwo("/member/logintwo"),
header: app.getRequestHeader(),
@@ -125,49 +98,22 @@ Page({
name: wx.getStorageSync('uid'),
phone: wx.getStorageSync('phone'),
storename: wx.getStorageSync('token')
},
success: function (res) {
var resp = res.data;
if (resp.code != 200) {
app.alert({
"content": resp.msg
});
return;
}
// 跳转
wx.navigateBack({});
}
})
},
//获取token
getUserBaseInfo: function () {
var that = this;
//假数据
// if (this.data.resp.erro == 0) {
// that.setData({
// infor: this.data.resp.infor
// });
// }
wx.request({
url: app.globalData.url + '/getUserToken',
header: app.getRequestHeader(),
request({
url: '/getUserToken',
method: 'GET',
data: {
phonenumber: '18133922183'
},
success: function (res) {
var resp = res.data;
console.log(resp + "=================dddddddddddddddddddddddddddddddddddddddddddd")
console.log(resp.erro + "=================dddddddddddddddddddddddddddddddddddddddddddd")
data: { phonenumber: this.data.phone },
loading: false
}).then(resp => {
if (resp.erro == 0) {
that.setData({
infor: resp.infor
});
that.setData({ infor: resp.infor })
}
}
});
}).catch(() => {})
},
/**

View File

@@ -1,36 +1,37 @@
//获取应用实例
var app = getApp();
const { request } = require('../../utils/request')
Page({
/**
* 页面的初始数据
*/
data: {
order_list: [],
nav_type: 1,
all: true,
height: '',
url: app.globalData.url,
statusType: ["全部", "待接单", "已接单", "服务中", "待支付", "已完成","已取消"],
status: ["", "0", "1", "2", "3","4","-2"],
currentType: 0,
tabClass: ["", "", "", "", "", "", ""],
pageNum: 1
pageNum: 1,
hasMore: true,
firstLoad: true,
loading: false
},
statusTap: function (e) {
var curType = e.currentTarget.dataset.index;
this.setData({
currentType: curType,
pageNum: 1
pageNum: 1,
order_list: []
});
this.getOrderList();
},
onClickLeft() {
wx.navigateBack()
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var statusBarHeight = wx.getSystemInfoSync().statusBarHeight;
this.setData({
@@ -38,142 +39,98 @@ Page({
height: 46 + statusBarHeight,
});
},
//条目点击---订单状态:-2:已取消 -1:拒绝接单 0:待接单 1:已接单 2:服务中 3:待支付 4:已完成 5申请退款 6退款中 7已退款 8:已结算
itemclick(e) {
console.log("列表条目的index==" + e.currentTarget.dataset.index)
//去订单详情页
if (e.currentTarget.dataset.item != 0) {
let item = JSON.stringify(e.currentTarget.dataset.item);
//console.log("传递的item" + item);
wx.navigateTo({
url: "/pages/orderdetail/orderdetail?item=" + item,
});
}
},
//跳转订单详情(去付款)
go_pay(e) {
console.log(e.currentTarget.dataset.id)
// wx.navigateTo({
// url: '/pages/orderdetail/orderdetail?' + "type=" + '2' + '&id=' + e.currentTarget.dataset.id,
// })
if (e.currentTarget.dataset.item != 0) {
let item = JSON.stringify(e.currentTarget.dataset.item);
console.log("传递的item" + item);
wx.navigateTo({
url: "/pages/orderdetail/orderdetail?item=" + item,
});
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
this.setData({
order_list: [],
pageNum: 1
})
this.getOrderList();
// 首次加载或从其他页返回刷新
if (this.data.firstLoad) {
this.setData({ firstLoad: false })
this.getOrderList()
}
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
//this.getOrderList();
var that = this;
wx.request({
url: app.globalData.url + '/system/view/list',
header: app.getRequestHeader(),
this.setData({ pageNum: 1 })
this.refreshList().finally(() => wx.stopPullDownRefresh())
},
onReachBottom: function () {
if (this.data.hasMore && !this.data.loading) {
this.getOrderList()
}
},
async refreshList() {
this.setData({ loading: true })
try {
var res = await request({
url: '/system/view/list',
method: 'GET',
loading: false,
data: {
pageNum: 1,
pageSize: 30,
status: that.data.status[that.data.currentType],
status: this.data.status[this.data.currentType],
usercId: wx.getStorageSync('uid')
},
success: (res) => {
var resp = res.data;
if (res.data.code == 200) {
that.setData({
pageNum: 1,
order_list: resp.rows || resp.data,
});
wx.stopPullDownRefresh({
success: (res) => {},
}
})
}
}
});
var list = res.rows || res.data || []
this.setData({
order_list: list,
pageNum: 2,
hasMore: list.length >= 30
})
} catch (e) {}
this.setData({ loading: false })
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
console.log("==页面上拉触底事件的处理函数==")
// this.getOrderList();
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
},
//订单状态:-2:已取消 -1:拒绝接单 0:待接单 1:已接单 2:服务中 3:待支付 4:已完成 5申请退款 6退款中 7已退款 8:已结算
getOrderList: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/view/list',
header: app.getRequestHeader(),
async getOrderList() {
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: that.data.status[that.data.currentType],
status: this.data.status[this.data.currentType],
usercId: wx.getStorageSync('uid')
},
success: (res) => {
var resp = res.data;
if (res.data.code == 200) {
if (this.data.pageNum == 1) {
that.setData({
order_list: resp.rows || resp.data,
pageNum: this.data.pageNum + 1
});
}
})
var newList = res.rows || res.data || []
if (this.data.pageNum === 1) {
this.setData({
order_list: newList,
pageNum: 2,
hasMore: newList.length >= 30
})
} else {
that.setData({
order_list: this.data.order_list.concat(resp.rows || resp.data),
pageNum: this.data.pageNum + 1
});
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 })
},
})

View File

@@ -35,7 +35,7 @@
<view style="height: 1rpx;background: #ECECEC;"></view>
<view class="details_box">
<image src="{{item.userbAvatar}}" class="Group-image"></image>
<image src="{{item.userbAvatar || '/pages/image/userimg.png'}}" class="Group-image"></image>
<view style="margin-left: 31rpx;margin-top: 40rpx;">
<view class="title">{{item.userbName}}</view>
<view class="detail_number">{{item.serviceTime}}天</view>

View File

@@ -1,11 +1,9 @@
//获取应用实例
var app = getApp();
// pages/peihu.js
const { request } = require('../../utils/request')
Page({
/**
* 页面的初始数据
*/
data: {
dingdan_msg: false,
ewm_msg: false,
@@ -20,6 +18,8 @@ Page({
refund: '',
charge_phone: '',
refund_box: false,
paying: false,
refund_reason: '',
list: [{
text: '不喜欢/不想要',
},
@@ -42,74 +42,50 @@ Page({
show: false,
height: ''
},
// 确认支付
confirm_pay: async function () {
var that = this;
if (!this.data.is_ok_click) {
that.setData({
is_ok_click: true
})
//你要执行的操作
confirm_pay() {
if (this.data.paying) return
this.setData({ paying: true })
this.godetail()
setTimeout(function () {
that.setData({
is_ok_click: false
})
}, 10000) //阻止一秒点击多次
} else {
// wx.showToast({
// title: '请勿重复操作!',
// })
}
},
godetail() {
console.log('点击了去付款');
var _this = this;
console.log(this.data.id + 'tttttttttttttttttttttttttttttttttt')
wx.request({
// url: app.globalData.url + '/system/weixinPayNew/12345666',
//order_id为8位数字
url: app.globalData.url + '/system/order/weixinPay/' + this.data.order_id,
header: app.getRequestHeader(),
async godetail() {
var _this = this
try {
// 1. 获取支付参数
var res = await request({
url: '/system/order/weixinPay/' + _this.data.order_id,
method: 'GET',
data: {
loading: true,
loadingText: '加载支付信息...',
showError: true
})
},
success(res) {
if (res.data.code == 200) {
console.log('timeStamp==' + res.data.data.timeStamp)
console.log('nonceStr==' + res.data.data.nonceStr)
console.log('package==' + res.data.data.package)
console.log('signType==' + res.data.data.signType)
console.log('paySign==' + res.data.data.paySign)
// 2. 调起微信支付
wx.requestPayment({
timeStamp: res.data.data.timeStamp,
nonceStr: res.data.data.nonceStr,
package: res.data.data.package,
signType: res.data.data.signType,
paySign: res.data.data.paySign,
success(res) {
console.log('支付成功');
wx.navigateTo({
timeStamp: res.data.timeStamp,
nonceStr: res.data.nonceStr,
package: res.data.package,
signType: res.data.signType,
paySign: res.data.paySign,
success() {
wx.redirectTo({
url: '/shopping/pages/paysuccess/paysuccess?order_id=' + _this.data.order_id + '&paid=1',
})
},
fail(res) {
console.log('支付失败');
}
})
} else {
fail(payErr) {
wx.showToast({
title: res.data.msg,
icon: 'error',
title: '支付取消或失败',
icon: 'none',
duration: 2000
})
}
}
})
} catch (e) {
// 错误提示已由 request 工具处理
}
_this.setData({ paying: false })
},
/**
@@ -189,24 +165,18 @@ Page({
},
//订单详情
getOrderDetail: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/view/getInfo',
header: app.getRequestHeader(),
method: 'get',
data: {
orderId: this.data.order_id
},
success: function (res) {
var resp = res.data;
if (res.data.code == 200) {
that.setData({
item: res.data.data
});
async getOrderDetail() {
try {
var res = await request({
url: '/system/view/getInfo',
method: 'GET',
loading: true,
data: { orderId: this.data.order_id }
})
if (res.data) {
this.setData({ item: res.data })
}
}
});
} catch (e) {}
},
/**
* 订单状态:0:待接单 1:已接单 时候的取消订单
@@ -357,6 +327,15 @@ Page({
mengceng: true
})
},
/**
* 已完成订单申请退款
*/
applyRefund() {
this.setData({
refund_box: true,
mengceng: true
})
},
/**
* 填写原因开始退款
*/
@@ -364,27 +343,36 @@ Page({
this.refund()
},
/**
* 退款
* 退款申请
*/
refund() {
var that = this;
var reason = this.data.refund_reason || '';
wx.request({
url: app.globalData.url + '/system/order/refundOrder/' + this.data.order_id,
header: app.getRequestHeader(),
method: 'GET',
url: app.globalData.url + '/system/order/applyRefund/' + this.data.order_id,
header: {
'content-type': 'application/json',
'Authorization': 'Bearer ' + app.getCache("token")
},
method: 'POST',
data: {
// id: this.data.item.orderId,
remark: reason
},
success: (res) => {
var resp = res.data;
if (res.data.code == 200) {
wx.showToast({
title: '退款申请成功',
title: resp.msg || '退款申请成功',
icon: 'none'
})
setTimeout(function () {
//要延时执行的代码
wx.navigateBack()
}, 2000) //延迟时间 这里是2秒
}, 2000)
} else {
wx.showToast({
title: resp.msg || '申请失败',
icon: 'none'
})
}
}
});
@@ -394,6 +382,13 @@ Page({
mengceng: false
})
},
radioChange(e) {
var idx = e.detail.value;
var list = this.data.list;
this.setData({
refund_reason: list[idx] ? list[idx].text : ''
})
},
nocancel() {
this.setData({
dingdan_msg: false,
@@ -415,26 +410,6 @@ Page({
phoneNumber: this.data.charge_phone,
})
},
//订单详情
getOrderDetail: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/view/getInfo/',
header: app.getRequestHeader(),
method: 'get',
data: {
orderId: this.data.order_id
},
success: function (res) {
var resp = res.data;
if (res.data.code == 200) {
that.setData({
item: res.data.data
});
}
}
});
},
/**
* 用户点击右上角分享
*/

View File

@@ -59,6 +59,13 @@
</view>
</view>
<!-- 订单状态:4:已完成 -->
<view wx:if="{{item.status==4}}" class="opt_view">
<view class="detail_btn" bindtap="applyRefund">
申请退款
</view>
</view>
<!-- 联系信息 -->
<view class="contact">
<view class="contact_title">联系信息</view>
@@ -153,7 +160,7 @@
<view wx:for="{{list}}" class="list_box">
<view>{{item.text}}</view>
<view>
<radio></radio>
<radio value="{{index}}" />
</view>
</view>
</radio-group>
@@ -173,7 +180,7 @@
<view wx:for="{{list}}" class="list_box">
<view>{{item.text}}</view>
<view>
<radio></radio>
<radio value="{{index}}" />
</view>
</view>
</radio-group>

View File

@@ -339,7 +339,12 @@ Page({
})
},
fail(res) {
console.log('支付失败');
console.log('支付失败', res);
wx.showToast({
title: '支付取消或失败',
icon: 'none',
duration: 2000
});
}
})
} else {

View File

@@ -1,70 +1,69 @@
//获取应用实例
var app = getApp();
const { request } = require('../../utils/request')
Page({
data: {
option1: [{
text: '切换医院',
value: 0
},
{
text: '西安市高新医院',
value: 1
},
{
text: '第四军医大学西京医院',
value: 2
},
{
text: '陕西省人民医院',
value: 3
},
],
option1: [{ text: '切换医院', value: 0 }],
value1: 0,
version_number: '1.0',
height: '',
url: app.globalData.url,
hospitalId:''
hospitalId: '',
special_offer: [],
firstLoad: true
},
shuChange: function (e) {
console.log(e.detail);
this.setData({
hospitalId: e.detail
})
this.getBannerAndCat();
this.setData({ hospitalId: e.detail })
this.getCaregiverList();
},
//生命周期回调——监听小程序启动或切前台。
onShow: function () {
this.getBannerAndCat();
if (this.data.firstLoad) {
this.setData({ firstLoad: false })
this.getHospitals()
this.getCaregiverList()
}
},
//首页信息
getBannerAndCat: function () {
// 加载医院列表
getHospitals: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/hospitalUserView/list',
header: app.getRequestHeader(),
request({
url: '/system/hospital/list',
method: 'GET',
data: {
hospitalId:this.data.hospitalId
loading: false
}).then(res => {
var hospitals = res.rows || res.data || [];
var options = [{ text: '切换医院', value: 0 }];
hospitals.forEach(function (h, i) {
options.push({
text: h.hospitalname,
value: i + 1
})
})
that.setData({ option1: options, hospitalData: hospitals })
}).catch(() => {})
},
success: (res) => {
var resp = res.data;
if (res.data.code == 200) {
that.setData({
special_offer: resp.data,
});
}
}
});
// 加载陪护人员列表
getCaregiverList: function () {
var that = this;
request({
url: '/system/hospitalUserView/list',
method: 'GET',
loading: true,
data: { hospitalId: this.data.hospitalId }
}).then(res => {
that.setData({ special_offer: res.data || [] })
}).catch(() => {})
},
// 跳转详情
details(e) {
if (e.currentTarget.dataset.item != 0) {
let item = JSON.stringify(e.currentTarget.dataset.item);
console.log("传递的item" + item);
wx.navigateTo({
url: "/shopping/pages/goddess/goddess?item=" + item,
});
@@ -78,15 +77,10 @@ Page({
this.setData({
statusBarHeight: statusBarHeight,
height: 46 + statusBarHeight,
hospitalId: e.hospitalId || ''
});
console.log('e.hospitalId' + e.hospitalId);
this.setData({
hospitalId: e.hospitalId
})
},
changeDateTime1(e) {
this.setData({
dateTime1: e.detail.value
});
this.setData({ dateTime1: e.detail.value });
},
})

View File

@@ -43,5 +43,9 @@
</view>
</view>
<view wx:else style="display:flex;flex-direction:column;align-items:center;padding-top:160rpx;">
<image style="width:200rpx;height:200rpx;" src="/pages/image/empty.png"></image>
<view style="color:#999;font-size:28rpx;margin-top:20rpx;">暂无陪护人员</view>
</view>
</view>
</view>

View File

@@ -1,102 +1,77 @@
//获取应用实例
var app = getApp();
const { request } = require('../../utils/request')
Page({
data: {
option1: [{
text: '全部医院',
value: 0
},
{
text: '西京医院',
value: 1
},
{
text: '陕西省人民医院',
value: 2
},
{
text: '陕西省人民医院1',
value: 5
},
],
option1: [{ text: '全部医院', value: 0 }],
value1: 0,
version_number: '1.0',
height: '',
url: app.globalData.url,
itemView: [
{
icon: "诊前约号",
title: "全程陪诊",
price:"200",
describe:"服务内容:排队 预约 挂号 缴费 取药 记录医嘱"
},
{
icon: "诊前约号",
title: "诊前约号",
price:"20",
describe:"服务内容:协助办理完成挂号"
},
// {
// icon: "代办买药",
// title: "代办买药",
// price:"30",
// describe:"服务内容:协助代理买药"
// },
{
icon: "取送结果",
title: "代办问诊",
price:"50",
describe:"服务内容:使用病人或者家属取检查报告化验单向医生问明情况"
},
{
icon: "取送结果",
title: "取送结果",
price:"50",
describe:"服务内容:包括给病人或者家属取检查报告化验单等"
},
// {
// icon: "出入院办理",
// title: "出入院办理",
// price:"100",
// describe:"协助办理出入院手续"
// }
],
itemView: [],
firstLoad: true
},
onChange(e) {
this.setData({
timeData: e.detail,
});
this.setData({ timeData: e.detail });
},
//生命周期回调——监听小程序启动或切前台。
onShow: function () {
this.getBannerAndCat();
},
//首页信息
getBannerAndCat: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/hospitalUserView/list',
header: app.getRequestHeader(),
method: 'GET',
data: {
onShow: function () {
if (this.data.firstLoad) {
this.setData({ firstLoad: false })
this.getHospitals()
this.getServiceTypes()
}
},
success: (res) => {
var resp = res.data;
if (res.data.code == 200) {
that.setData({
special_offer: resp.data,
});
// 加载医院列表(用于下拉选择)
getHospitals: function () {
var that = this;
request({
url: '/system/hospital/list',
method: 'GET',
loading: false
}).then(res => {
var hospitals = res.rows || res.data || [];
var options = [{ text: '全部医院', value: 0 }];
hospitals.forEach(function (h, i) {
options.push({
text: h.hospitalname,
value: i + 1
})
})
that.setData({ option1: options, hospitalData: hospitals })
}).catch(() => {})
},
// 加载服务类型
getServiceTypes: function () {
var that = this;
request({
url: '/system/serviceType/enabled',
method: 'GET',
data: { category: '2' },
loading: false
}).then(res => {
var services = res.data || [];
if (services.length) {
var mapped = services.map(function (s) {
return {
icon: s.icon || s.name,
title: s.name,
price: s.price,
describe: s.description || ''
}
})
that.setData({ itemView: mapped })
}
});
}).catch(() => {})
},
// 跳转详情
details(e) {
if (e.currentTarget.dataset.item != 0) {
let item = JSON.stringify(e.currentTarget.dataset.item);
console.log("传递的item" + item);
wx.navigateTo({
url: "/pages/peizhen/peizhendetail?item=" + item,
});
@@ -113,8 +88,6 @@ Page({
});
},
changeDateTime1(e) {
this.setData({
dateTime1: e.detail.value
});
this.setData({ dateTime1: e.detail.value });
},
})

View File

@@ -21,14 +21,12 @@
<view class="contar">
<view class="tejiashangpin">
<view class="Discount" wx:if="{{itemView.length}}">
<view wx:for="{{itemView}}" wx:key="{{index}}" wx:for-item="item" class="flex-item bc_green" style='display:flex;background-color: white;margin-right: 10rpx;margin-bottom: 24rpx;'>
<view class="Group" bindtap="details" data-item="{{item}}">
<view class="Group-image">
<image src="../image/abc.png"></image>
</view>
<view class="content">
<view style="display: flex; flex-direction:row;align-items : flex-end;">
<view class="title1">{{item.title}}</view>
<view class="title2"></view>
@@ -42,18 +40,10 @@
</view>
</view>
</view>
</view>
<view wx:else style="display:flex;flex-direction:column;align-items:center;padding-top:160rpx;">
<image style="width:200rpx;height:200rpx;" src="/pages/image/empty.png"></image>
<view style="color:#999;font-size:28rpx;margin-top:20rpx;">暂无服务</view>
</view>
</view>
</view>

View File

@@ -1,18 +1,16 @@
const app = getApp()
const { request } = require('../../utils/request')
Page({
/**
* 页面的初始数据
*/
data: {
task: {
phone: '',
phonedisplay: false,
placeholder: '请输入联系方式'
},
objectArray: [{ text: '加载中...', value: 0 }], // 医院列表(动态加载)
objectIndex: 0, // 默认显示位置
objectArray: [{ text: '请选择医院', value: 0 }],
objectIndex: 0,
show: true,
height: '',
seckill_start: '请输入开始日期',
@@ -33,16 +31,17 @@ Page({
id: 0,
order_id: '',
timeData: {},
Height: "", //这是swiper要动态设置的高度属性
Height: "",
charge_phone: '',
is_ok_click: false,
submitting: false,
clock: '',
address: '',
danjia: 0.01, //单位为元
danjia: 0.01,
beizhu: '',
name: '',
jzphone: '',
idcard: '' // 患者身份证号(可选)
idcard: '',
hospitalsLoaded: false
},
inputtaskphone(e) {
@@ -85,58 +84,29 @@ Page({
})
},
// 确认支付
confirm_payy: async function () {
// wx.showToast({
// title: '点击了',
// icon: 'none'
// });
var that = this;
if (!this.data.is_ok_click) {
that.setData({
is_ok_click: true
})
//你要执行的操作
// 提交订单
confirm_payy() {
if (this.data.submitting) return
this.payment()
setTimeout(function () {
that.setData({
is_ok_click: false
})
}, 10000) //阻止一秒点击多次
} else {
// wx.showToast({ title:'请勿重复操作!', icon: 'none' });
}
},
//加载医院列表
loadHospitals: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/hospital/list',
header: app.getRequestHeader(),
request({
url: '/system/hospital/list',
method: 'GET',
data: {},
success: (res) => {
console.log('医院列表响应:', res.statusCode, JSON.stringify(res.data).substring(0, 200));
if (res.data.code == 200) {
var hospitals = res.data.rows || res.data.data || [];
loading: false
}).then(res => {
var hospitals = res.rows || res.data || [];
var list = [{ text: '请选择医院', value: 0 }];
hospitals.forEach(function (h) {
list.push({
text: h.hospitalname,
value: h.id
list.push({ text: h.hospitalname, value: h.id });
});
});
that.setData({ objectArray: list });
} else {
console.error('医院列表加载失败 code:', res.data.code, 'msg:', res.data.msg);
that.setData({ objectArray: list, hospitalsLoaded: true });
}).catch(() => {
that.setData({ objectArray: [{ text: '加载失败,点击重试', value: 0 }] });
}
},
fail: (err) => {
console.error('医院列表网络错误:', err);
that.setData({ objectArray: [{ text: '网络错误,点击重试', value: 0 }] });
}
});
})
},
getprice: function () {
var that = this;
@@ -190,80 +160,54 @@ Page({
return true
}
},
payment() {
async payment() {
// 表单校验
if (this.data.objectIndex == 0) {
wx.showToast({
title: '请输选择医院',
icon: 'none'
});
wx.showToast({ title: '请选择医院', icon: 'none' })
return
}
if (this.data.jzphone == '') {
wx.showToast({
title: '请输入就诊人电话',
icon: 'none'
});
if (!this.data.jzphone) {
wx.showToast({ title: '请输入就诊人电话', icon: 'none' })
return
}
if (!/^1[3-9]\d{9}$/.test(this.data.jzphone)) {
wx.showToast({ title: '手机号码格式不正确', icon: 'none' })
return
}
if (this.data.seckill_start == '请输入开始日期') {
wx.showToast({ title: '请选择开始日期', icon: 'none' })
return
}
var regtel = new RegExp('(^1[3|4|5|7|8][0-9]{9}$)', 'g');
if (!regtel.exec(this.data.jzphone )) {
wx.showToast({
title: '手机号码格式不正确!',
icon: 'none'
});
return
}
if (this.data.seckill_start == "请输入开始日期") {
wx.showToast({
title: '请输入开始日期',
icon: 'none'
});
return
}
var that = this;
wx.request({
url: app.globalData.url + '/system/order/insertOrderPz',
this.setData({ submitting: true })
try {
var res = await request({
url: '/system/order/insertOrderPz',
method: 'POST',
loading: true,
loadingText: '提交订单中...',
data: {
cId: wx.getStorageSync('uid'), //患者id
yuguMoney: this.data.item.price, //金额
yuliu9: "2", //订单大类1-陪护 2-陪诊
yuliu10: this.data.item.ordertype, //订单类型1-全程陪诊,2-诊前约号3-送取报告4-待办问诊
cId: wx.getStorageSync('uid'),
yuguMoney: this.data.item.price,
yuliu9: "2",
yuliu10: this.data.item.ordertype,
hospitalId: this.data.objectArray[this.data.objectIndex].value,
startTime: this.data.seckill_start,
yuliu11: this.data.jzphone, //患者电话
yuliu12: this.data.beizhu, //特殊要求
yuliu13: this.data.address, //送达地址
yuliu14: this.data.idcard //患者身份证号(可选)
yuliu11: this.data.jzphone,
yuliu12: this.data.beizhu,
yuliu13: this.data.address,
yuliu14: this.data.idcard
},
method: 'POST',
header: {
'content-type': 'application/json',
'Authorization': 'Bearer ' + app.getCache("token")
},
success(res) {
if (res.data.code == 200) {
that.setData({
order_id: res.data.data.orderId,
header: { 'content-type': 'application/json' }
})
console.log('order_id==' + res.data.data.orderId)
app.console(that.data.order_id + "======要传的order_id")
wx.navigateTo({
url: '/shopping/pages/paysuccess/paysuccess?' + "order_id=" + that.data.order_id,
});
} else {
wx.showToast({
title: res.data.msg,
icon: 'error',
duration: 2000
// redirectTo 防止返回重复下单
wx.redirectTo({
url: '/shopping/pages/paysuccess/paysuccess?order_id=' + res.data.orderId,
})
} catch (e) {
// 错误提示已由 request 处理
this.setData({ submitting: false })
}
}
})
},
bindDateChange: function (e) {
@@ -340,10 +284,9 @@ Page({
* 生命周期函数--监听页面显示
*/
onShow: function () {
var that = this;
this.loadHospitals();
this.getprice();
this.getpriceType();
if (!this.data.hospitalsLoaded) {
this.loadHospitals()
}
},
/**

View File

@@ -0,0 +1,87 @@
// 服务模块
var app = getApp();
const { request } = require('../../utils/request')
Page({
data: {
servicesPeizhen: [],
servicesPeihu: [],
activeTab: 0,
firstLoad: true
},
onShow() {
if (this.data.firstLoad) {
this.setData({ firstLoad: false })
this.loadServices()
}
},
onPullDownRefresh() {
this.loadServices().finally(() => wx.stopPullDownRefresh())
},
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 })
])
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
}
}),
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
}
})
})
} catch (e) {}
},
switchTab(e) {
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
}
var item = JSON.stringify(e.currentTarget.dataset.item)
if (this.data.activeTab === 0) {
wx.navigateTo({ url: '/pages/peizhen/peizhendetail?item=' + item })
} else {
wx.navigateTo({ url: '/shopping/pages/goddess/goddess?item=' + item })
}
}
})

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "服务项目"
}

View File

@@ -0,0 +1,69 @@
<view class="page">
<!-- 顶部标题 -->
<view class="header">
<view class="header-title">全部服务</view>
<view class="header-sub">专业陪诊陪护,全程无忧</view>
</view>
<!-- 分类切换 -->
<view class="tab-bar">
<view class="tab-item {{activeTab === 0 ? 'active' : ''}}" bindtap="switchTab" data-index="{{0}}">
<text>就医陪诊</text>
<view class="tab-line" wx:if="{{activeTab === 0}}"></view>
</view>
<view class="tab-item {{activeTab === 1 ? 'active' : ''}}" bindtap="switchTab" data-index="{{1}}">
<text>院内陪护</text>
<view class="tab-line" wx:if="{{activeTab === 1}}"></view>
</view>
</view>
<!-- 陪诊服务 -->
<view class="service-list" wx:if="{{activeTab === 0}}">
<block wx:for="{{servicesPeizhen}}" wx:key="id">
<view class="service-card" bindtap="onServiceTap" data-item="{{item}}">
<view class="card-left">
<view class="card-icon">
<image src="{{item.icon || '/pages/image/empty.png'}}" mode="aspectFit"></image>
</view>
</view>
<view class="card-body">
<view class="card-name">{{item.name}}</view>
<view class="card-desc">{{item.desc}}</view>
</view>
<view class="card-right">
<view class="card-price">¥{{item.price}}</view>
<view class="card-btn">预约</view>
</view>
</view>
</block>
<view wx:if="{{!servicesPeizhen.length}}" class="empty">
<image src="/pages/image/empty.png" style="width:200rpx;height:200rpx;"></image>
<text>暂无服务</text>
</view>
</view>
<!-- 陪护服务 -->
<view class="service-list" wx:if="{{activeTab === 1}}">
<block wx:for="{{servicesPeihu}}" wx:key="id">
<view class="service-card" bindtap="onServiceTap" data-item="{{item}}">
<view class="card-left">
<view class="card-icon">
<image src="{{item.icon || '/pages/image/empty.png'}}" mode="aspectFit"></image>
</view>
</view>
<view class="card-body">
<view class="card-name">{{item.name}}</view>
<view class="card-desc">{{item.desc}}</view>
</view>
<view class="card-right">
<view class="card-price">¥{{item.price}}</view>
<view class="card-btn">预约</view>
</view>
</view>
</block>
<view wx:if="{{!servicesPeihu.length}}" class="empty">
<image src="/pages/image/empty.png" style="width:200rpx;height:200rpx;"></image>
<text>暂无服务</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,134 @@
.page {
min-height: 100vh;
background: #f5f6fa;
padding-bottom: 20rpx;
}
.header {
background: linear-gradient(135deg, #4A90D9, #357ABD);
padding: 60rpx 40rpx 40rpx;
color: #fff;
}
.header-title {
font-size: 40rpx;
font-weight: bold;
}
.header-sub {
font-size: 24rpx;
margin-top: 10rpx;
opacity: 0.85;
}
.tab-bar {
display: flex;
background: #fff;
padding: 0 30rpx;
border-bottom: 1rpx solid #eee;
}
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
}
.tab-item.active {
color: #4A90D9;
font-weight: bold;
}
.tab-line {
width: 60rpx;
height: 6rpx;
background: #4A90D9;
border-radius: 3rpx;
margin: 8rpx auto 0;
}
.service-list {
padding: 24rpx;
}
.service-card {
display: flex;
align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 28rpx 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.card-left {
margin-right: 20rpx;
}
.card-icon {
width: 90rpx;
height: 90rpx;
border-radius: 16rpx;
background: #f0f4ff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.card-icon image {
width: 60rpx;
height: 60rpx;
}
.card-body {
flex: 1;
overflow: hidden;
}
.card-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.card-desc {
font-size: 24rpx;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-right {
text-align: center;
margin-left: 16rpx;
}
.card-price {
font-size: 32rpx;
font-weight: bold;
color: #FF8F1F;
margin-bottom: 10rpx;
}
.card-btn {
background: linear-gradient(135deg, #4A90D9, #357ABD);
color: #fff;
font-size: 24rpx;
padding: 8rpx 28rpx;
border-radius: 30rpx;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 160rpx;
color: #999;
font-size: 28rpx;
}

View File

@@ -1,194 +1,62 @@
//获取应用实例
var app = getApp();
const { request } = require('../../utils/request')
Page({
/**
* 页面的初始数据
*/
data: {
resp:{
"currentPage": 1,
"detail": [
{
"account_type_name": "微信提现",
"money": "-5.79",
"time": "2022-02-24 10:40:03",
"type": "-1"
},
{
"account_type_name": "订单退款",
"money": "0.01",
"time": "2022-02-23 15:22:05",
"type": "1"
},
{
"account_type_name": "购买商品",
"money": "-0.01",
"time": "2022-02-23 09:27:31",
"type": "-1"
},
{
"account_type_name": "购买商品",
"money": "-0.10",
"time": "2022-02-22 11:48:20",
"type": "-1"
},
{
"account_type_name": "订单退款",
"money": "0.01",
"time": "2022-02-18 16:31:09",
"type": "1"
},
{
"account_type_name": "订单退款",
"money": "0.01",
"time": "2022-02-17 11:06:04",
"type": "1"
},
{
"account_type_name": "订单退款",
"money": "2.00",
"time": "2022-02-17 10:59:13",
"type": "1"
},
{
"account_type_name": "购买商品",
"money": "-2.00",
"time": "2022-02-16 10:31:46",
"type": "-1"
},
{
"account_type_name": "订单退款",
"money": "2.00",
"time": "2022-02-16 10:25:33",
"type": "1"
},
{
"account_type_name": "购买商品",
"money": "-2.00",
"time": "2022-02-16 10:01:25",
"type": "-1"
}
],
"erro": 0,
"lastPage": 6,
"msg": "获取数据成功",
"total": 52
},
order_list:[],
order_list: [],
navHeight: '',
menuButtonInfo: {},
searchMarginTop: 0, // 搜索框上边距
searchWidth: 0, // 搜索框宽度
searchHeight: 0 // 搜索框高度
searchMarginTop: 0,
searchWidth: 0,
searchHeight: 0,
firstLoad: true
},
back(){
back() {
wx.navigateBack()
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
wx.setNavigationBarTitle({
title: '我的钱包'
})
wx.setNavigationBarTitle({ title: '我的钱包' })
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
this.getGoodsList();
if (this.data.firstLoad) {
this.setData({ firstLoad: false })
this.getGoodsList()
}
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
this.getGoodsList().finally(() => wx.stopPullDownRefresh())
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
},
// 跳转详情
activity(e){
activity(e) {
if (e.currentTarget.dataset.id != 0) {
console.log("你选择的课程id是"+e.currentTarget.dataset.id);
wx.navigateTo({
url: "/shopping/pages/goddess/goddess?id=" + e.currentTarget.dataset.id,
});
}
},
//商品列表
// 交易明细列表
getGoodsList: function () {
var that = this;
//假数据
if(this.data.resp.erro==0){
that.setData({
order_list:this.data.resp.detail
});
}
wx.request({
url: app.buildUrl("/app/Goddesswechat/getCapitalDetails"),
header: app.getRequestHeader(),
method:'POST',
return request({
url: '/app/Goddesswechat/getCapitalDetails',
method: 'POST',
loading: true,
data: {
version_number: this.data.version_number,
uid:wx.getStorageSync('uid'),
token:wx.getStorageSync('token'),
page:'1',
page_size:10,
},
success: function (res) {
var resp = res.data;
console.log(resp+"=================dddddddddddddddddddddddddddddddddddddddddddd")
console.log(resp.erro+"=================dddddddddddddddddddddddddddddddddddddddddddd")
if(resp.erro==0){
that.setData({
order_list:resp.detail
});
uid: wx.getStorageSync('uid'),
token: wx.getStorageSync('token'),
page: '1',
page_size: 10,
}
}).then(res => {
if (res.erro === 0) {
that.setData({ order_list: res.detail || [] })
}
});
}).catch(() => {})
},
})

View File

@@ -2,19 +2,19 @@
<view class="order-list" wx:if="{{order_list.length}}">
<view wx:for="{{order_list}}" wx:key="{{index}}" wx:for-item="item">
<view class="Group" bindtap="activity" data-id="{{item.id}}">
<view style="margin-left: 34rpx;">
<view wx:if="{{item.type==1}}" class="title1">退款到账</view>
<view wx:if="{{item.type==-1}}" class="title1">提现</view>
<view class="title2">{{item.time}}</view>
</view>
<view style="position: absolute;right: 24rpx;">
<view class="Price1">¥{{item.money}}</view>
</view>
</view>
</view>
</view>
</view>
<view wx:else class="no-order" style="display:flex;flex-direction:column;align-items:center;padding-top:200rpx;">
<image style="width:200rpx;height:200rpx;" src="/pages/image/empty.png"></image>
<view style="color:#999;font-size:28rpx;margin-top:20rpx;">暂无交易记录</view>
</view>
</view>

View File

@@ -1,10 +1,8 @@
const app = getApp()
const { request } = require('../../../utils/request')
Page({
/**
* 页面的初始数据
*/
data: {
height: '',
seckill_start: '请输入开始日期',
@@ -26,175 +24,81 @@ Page({
id: 0,
order_id: '',
timeData: {},
Height: "", //这是swiper要动态设置的高度属性
Height: "",
charge_phone: '',
is_ok_click: false,
submitting: false,
clock: '',
address: '',
danjia: 0.01,//单位为元
danjia: 0.01,
beizhu: '',
},
// 确认支付
confirm_pay: async function () {
var that = this;
if (!this.data.is_ok_click) {
that.setData({
is_ok_click: true
})
//你要执行的操作
confirm_pay() {
if (this.data.submitting) return
this.payment()
setTimeout(function () {
that.setData({
is_ok_click: false
})
}, 10000) //阻止一秒点击多次
} else {
// wx.showToast({ title:'请勿重复操作!', icon: 'none' });
}
},
//获取单价
getprice: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/config/configKey/price',
header: app.getRequestHeader(),
method: 'GET',
data: {},
success: (res) => {
var resp = res.data;
if (res.data.code == 200) {
that.setData({
special_offer: resp.data,
});
}
}
});
},
//获取类型
getpriceType: function () {
var that = this;
wx.request({
url: app.globalData.url + '/system/config/configKey/priceType',
header: app.getRequestHeader(),
method: 'GET',
data: {},
success: (res) => {
var resp = res.data;
if (res.data.code == 200) {
that.setData({
special_offer: resp.data,
});
}
}
});
},
payment() {
async payment() {
if (this.data.seckill_start == "请输入开始日期") {
wx.showToast({
title: '请输入开始日期',
icon: 'none'
});
wx.showToast({ title: '请选择开始日期', icon: 'none' })
return
}
if (this.data.seckill_endx == "请输入结束日期") {
wx.showToast({
title: '请输入结束日期',
icon: 'none'
});
wx.showToast({ title: '请选择结束日期', icon: 'none' })
return
}
var that = this;
wx.request({
url: app.globalData.url + '/system/order',
// 校验日期先后
var start = new Date(this.data.seckill_start.replace(/-/g, "/"))
var end = new Date(this.data.seckill_endx.replace(/-/g, "/"))
if (end < start) {
wx.showToast({ title: '结束日期不能早于开始日期', icon: 'none' })
return
}
this.setData({ submitting: true })
try {
var res = await request({
url: '/system/order',
method: 'POST',
loading: true,
loadingText: '提交订单中...',
data: {
startTime: this.data.seckill_start,
endTime: this.data.seckill_endx,
bId: this.data.item.userId,
yuguMoney: this.data.paymoney,
price: this.data.danjia,//
priceType: "1",//1是按天
price: this.data.danjia,
priceType: "1",
hospitalId: this.data.item.hospitalId,
cId: wx.getStorageSync('uid'),
remark: this.data.beizhu,
},
method: 'POST',
header: {
'content-type': 'application/json',
'Authorization': 'Bearer ' + app.getCache("token")
},
success(res) {
if (res.data.code == 200) {
that.setData({
order_id: res.data.data.orderId,
header: { 'content-type': 'application/json' }
})
console.log('order_id==' + res.data.data.orderId)
app.console(that.data.order_id + "======要传的order_id")
wx.navigateTo({
url: '/shopping/pages/paysuccess/paysuccess?' + "order_id=" + that.data.order_id,
});
// console.log('timeStamp==' + res.data.data.timeStamp)
// console.log('nonceStr==' + res.data.data.nonceStr)
// console.log('package==' + res.data.data.package)
// console.log('signType==' + res.data.data.signType)
// console.log('paySign==' + res.data.data.paySign)
// wx.requestPayment({
// timeStamp: res.data.data.timeStamp,
// nonceStr: res.data.data.nonceStr,
// package: res.data.data.package,
// signType: res.data.data.signType,
// paySign: res.data.data.paySign,
// success(res) {
// console.log('支付成功');
// app.console(that.data.order_id + "======要传的order_id")
// wx.navigateTo({
// url: '/shopping/pages/paysuccess/paysuccess?' + "order_id=" + that.data.order_id,
// });
// },
// fail(res) {
// console.log('支付失败');
// }
// })
} else {
wx.showToast({
title: res.data.msg,
icon: 'error',
duration: 2000
wx.redirectTo({
url: '/shopping/pages/paysuccess/paysuccess?order_id=' + res.data.orderId,
})
} catch (e) {
this.setData({ submitting: false })
}
}
})
},
bindDateChange: function (e) {
this.setData({
seckill_start: e.detail.value
})
this.setData({ seckill_start: e.detail.value })
},
bindDateChangetwo: function (e) {
this.setData({
seckill_endx: e.detail.value,
})
this.setData({ seckill_endx: e.detail.value })
var day = this.checkDate()
var totalmoney = this.data.danjia* day;
var money = totalmoney.toFixed(2)
this.setData({
paymoney: money
})
var money = (this.data.danjia * day).toFixed(2)
this.setData({ paymoney: money })
},
checkDate: function () {
//日期格式化
var start_date = new Date(this.data.seckill_start.replace(/-/g, "/"));
var end_date = new Date(this.data.seckill_endx.replace(/-/g, "/"));
//转成毫秒数,两个日期相减
var ms = end_date.getTime() - start_date.getTime();
//转换成天数
var day = parseInt(ms / (1000 * 60 * 60 * 24));
//do something
console.log("day = ", day);
return day + 1
return Math.max(day + 1, 0)
},
onClickLeft() {
wx.navigateBack()
@@ -242,9 +146,6 @@ Page({
* 生命周期函数--监听页面显示
*/
onShow: function () {
var that = this;
this.getprice();
this.getpriceType();
},
/**

View File

@@ -2,8 +2,8 @@
const config = {
// 开发环境(本地后端,需改为本机IP)
// baseUrl: 'http://192.168.1.100:8039',
// 生产环境
baseUrl: 'http://101.43.95.130:8039',
// 生产环境(通过 nginx 代理,HTTPS + 域名)
baseUrl: 'https://ruilaizipj.com/prod-api',
}
module.exports = config

89
coupon/utils/request.js Normal file
View File

@@ -0,0 +1,89 @@
/**
* 通用网络请求工具
* - 自动 loading / hideLoading
* - 自动 fail 错误提示
* - Promise 风格,支持 async/await
* - code != 200 自动 toast 并 reject
*/
const app = getApp()
// 请求计数,支持并发 loading
let requestCount = 0
/**
* @param {object} opts
* @param {string} opts.url - API 路径(不含 baseUrl)
* @param {string} opts.method - GET/POST,默认 GET
* @param {object} opts.data - 请求参数
* @param {boolean} opts.loading - 是否显示 loading,默认 true
* @param {string} opts.loadingText - loading 文案,默认 "加载中..."
* @param {boolean} opts.showError - 失败时是否自动 toast,默认 true
* @param {object} opts.header - 自定义 header(会合并默认 header)
* @returns {Promise<object>} res.data - 响应体 { code, msg, data }
*/
function request(opts) {
const {
url,
method = 'GET',
data = {},
loading = true,
loadingText = '加载中...',
showError = true,
header = {}
} = opts
if (loading) {
if (requestCount === 0) {
wx.showLoading({ title: loadingText, mask: true })
}
requestCount++
}
return new Promise((resolve, reject) => {
wx.request({
url: app.globalData.url + url,
method,
data,
header: {
'content-type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer ' + app.getCache('token'),
...header
},
success(res) {
if (res.data.code === 200) {
resolve(res.data)
} else {
if (showError) {
wx.showToast({
title: res.data.msg || '请求失败',
icon: 'none',
duration: 2000
})
}
reject(res.data)
}
},
fail(err) {
if (showError) {
wx.showToast({
title: '网络异常,请稍后重试',
icon: 'none',
duration: 2000
})
}
reject(err)
},
complete() {
if (loading) {
requestCount--
if (requestCount <= 0) {
requestCount = 0
wx.hideLoading()
}
}
}
})
})
}
module.exports = { request }

View File

@@ -0,0 +1,68 @@
# Spring Security anonymous() vs permitAll() 踩坑记录
## 现象
后台管理系统登录后,访问 `业务管理 > 医院管理` 页面提示"系统接口异常",后端返回:
```json
{
"timestamp": "2026-05-25T03:08:28.944+08:00",
"status": 403,
"error": "Forbidden",
"path": "/system/hospital/list"
}
```
直接用 curl 不带 token 调用接口正常返回 200,但带上 JWT token 就返回 403。
## 根因
`SecurityConfig.java` 中 `/system/hospital/list` 被配置为 `.anonymous()`:
```java
.antMatchers("/login", ..., "/system/hospital/list", ...).anonymous()
```
**Spring Security 中两种访问控制的区别:**
| 配置 | 含义 | 匿名用户 | 已认证用户 |
|------|------|----------|------------|
| `.permitAll()` | 所有用户均可访问 | 允许 | 允许 |
| `.anonymous()` | 仅匿名用户可访问 | 允许 | **拒绝(403)** |
当后台管理员登录后,JWT 过滤器为请求注入了认证信息(不再是匿名用户),Spring Security 判定已认证用户无权访问 `.anonymous()` 资源,返回 403。
而小程序端调用该接口时不带 token(匿名),所以一直正常。
## 修复
将需要同时被"小程序(匿名)"和"后台(已认证)"访问的接口,从 `.anonymous()` 移至 `.permitAll()`:
```java
// 仅匿名用户 —— 登录、注册、验证码等
.antMatchers("/login","/appLogin","/system/msm","/smsLogin",
"/register","/captchaImage","/weixinLogin","/getUserToken",
"/system/weixin/wxPayNotify","/system/weixin/wxRefundNotify")
.anonymous()
// 所有用户均可访问 —— 被小程序和后台共同使用的数据接口
.antMatchers("/system/user/getAppIndexInfo","/system/hospital/list",
"/system/config/configKey/**","/system/nursing/published",
"/system/nursing/view/**","/system/serviceType/enabled")
.permitAll()
```
## 经验总结
1. **`.anonymous()` 是排他性的** —— 已登录用户访问会直接 403,不要用于需要同时支持前台和后台的 API
2. **`.permitAll()` vs `.anonymous()`** 选择标准:
- 登录/注册页面等"仅限未登录用户" → `.anonymous()`
- 公开数据接口(小程序+后台共用) → `.permitAll()`
3. **排查 403 时先看 SecurityConfig** —— 即使 API 直接 curl 正常,带 token 就可能因认证状态不同而变化
4. **排查思路**:逐层验证 API → 数据库 → 菜单路由 → Nginx 代理 → Security 权限配置
## 相关文件
- `rlz/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java`
- `rlz-ui/src/views/system/hospital/index.vue`
- `rlz-ui/src/utils/request.js` (前端错误拦截器)

View File

@@ -0,0 +1,19 @@
-- 服务类型表增加 icon 字段
ALTER TABLE rlz_service_type ADD COLUMN IF NOT EXISTS icon VARCHAR(255) DEFAULT '' COMMENT '服务图标URL';
-- 插入默认服务数据(如无数据)
INSERT INTO rlz_service_type (name, category, type_code, price, description, status, sort_order)
SELECT '全程陪诊', '2', '1', 200.00, '服务内容:排队 预约 挂号 缴费 取药 记录医嘱', '0', 1
WHERE NOT EXISTS (SELECT 1 FROM rlz_service_type WHERE type_code = '1');
INSERT INTO rlz_service_type (name, category, type_code, price, description, status, sort_order)
SELECT '诊前约号', '2', '2', 20.00, '服务内容:协助办理完成挂号', '0', 2
WHERE NOT EXISTS (SELECT 1 FROM rlz_service_type WHERE type_code = '2');
INSERT INTO rlz_service_type (name, category, type_code, price, description, status, sort_order)
SELECT '取送结果', '2', '3', 50.00, '服务内容:包括给病人或者家属取检查报告化验单等', '0', 3
WHERE NOT EXISTS (SELECT 1 FROM rlz_service_type WHERE type_code = '3');
INSERT INTO rlz_service_type (name, category, type_code, price, description, status, sort_order)
SELECT '代办问诊', '2', '4', 100.00, '服务内容:使用病人或者家属取检查报告化验单向医生问明情况', '0', 4
WHERE NOT EXISTS (SELECT 1 FROM rlz_service_type WHERE type_code = '4');

View File

@@ -0,0 +1,195 @@
# 瑞来健康陪诊平台 — 商业化评估报告
> 评估日期:2026-05-25
> 评估范围:后端(Spring Boot)、管理后台(Vue2)、Android App、微信小程序
---
## 一、项目现状总览
| 维度 | 状态 | 说明 |
|------|------|------|
| 技术栈 | 可维护 | Java 8 / Spring Boot 2.5.14 / Vue 2.6 / MyBatis / MySQL / 微信原生小程序 / Android Java |
| 部署 | 已跑通 | Tencent Cloud CVM + CynosDB + Docker Compose + Nginx |
| 后台管理 | 较完整 | RuoYi-Vue 框架,含医院/订单/财务/服务类型/护理资讯管理 |
| 微信小程序 | 部分完成 | 14 页面 + 3 Tab,核心流程可走,但存在假数据和空壳页面 |
| Android App | 需重构 | 从家装项目改造而来,遗留大量无关代码 |
| 安全性 | **不达标** | 支付密钥硬编码、JWT 弱密钥、手机号明文存储等多项致命问题 |
---
## 二、安全问题(P0 — 商用前必须解决)
### 2.1 致命级
| # | 问题 | 位置 | 风险 |
|---|------|------|------|
| 1 | 微信支付 RSA 私钥 + APIv3 Key 硬编码在源码中 | `WXPayConstantsAAA.java:23-50`、`PaySign.java:25-52`、`SysWeixinPayControllerNewBBBBB.java:34-42` | 源码泄露 = 资金被盗 |
| 2 | JWT 签名密钥 `abcdefghijklmnopqrstuvwxyz` | `application.yml:96` | 可暴力破解,伪造任意用户 Token |
| 3 | 短信接口无限流保护 | `MsmController.java` — 全平台零限流 | 可被刷短信,造成资损 |
### 2.2 高级
| # | 问题 | 位置 | 风险 |
|---|------|------|------|
| 4 | 手机号明文存储 + API 返回明文 + `System.out.print` 打印 | `sys_user.phonenumber`、`SysLoginController.java:76,123`、`MsmController.java:34` | 用户隐私泄露 |
| 5 | SMS 验证码明文打印到控制台 | `MsmController.java:46` | 验证码泄露 |
| 6 | 操作日志表 `sys_oper_log` 记录完整请求体(含密码、手机号) | `LogAspect.java:142-154` | 日志库成为敏感数据泄露源 |
| 7 | WeChat AppSecret 硬编码在 URL 参数中 | `SysLoginController.java:178,226` | AppSecret 泄露 |
| 8 | Android WeChat AppSecret 硬编码 | `WXEntryActivity.java:46` | AppSecret 泄露 |
### 2.3 中级
| # | 问题 | 位置 | 风险 |
|---|------|------|------|
| 9 | Druid 监控面板生产环境公开可访问(无 IP 白名单) | `SecurityConfig.java:116`、`application-docker.yml:53` | 数据库连接池信息暴露 |
| 10 | Swagger API 文档生产环境公开 | `application.yml:118`、`SecurityConfig.java:116` | API 结构暴露 |
| 11 | CSRF 全局关闭 + `X-Frame-Options` 关闭 | `SecurityConfig.java:104,120` | CSRF / 点击劫持风险 |
| 12 | ~95% 接口 `@PreAuthorize` 被注释 | 全平台 Controller | 任意认证用户可调用任意接口,无细粒度权限控制 |
---
## 三、小程序功能缺陷(P1)
| # | 页面 | 问题 | 影响 |
|---|------|------|------|
| 1 | `reforderdetail` 退款订单详情 | **全假数据**,真实 API 调用被整段注释 | 退款流程完全不可用 |
| 2 | `tuiguang/extensionInfor` 推广页 | **完全空壳**,所有生命周期函数为空 | 推广模块不存在 |
| 3 | `guanyu/guanyu` 关于/反馈页 | 表单提交调用不存在的函数 `this.payment()` | 反馈功能不可用 |
| 4 | `mechanism/mechanism` 机构页 | 机构详情跳转到不存在的 `pages/institutions/institutions` | 机构详情 404 |
| 5 | `peizhen/peizhen` + `peihu/peihu` | 医院选择器数据硬编码(不从后台接口读取) | 数据不实时 |
| 6 | `index/index` | 服务价格硬编码(200/20/100/50) | 调价需改代码发版 |
| 7 | `myshoucang/myshoucang_index` | 收藏列表全假数据 | 收藏功能不可用 |
| 8 | `my/my` | 砍价功能写死"暂无开放" | 营销功能缺失 |
| 9 | `orderdetail/orderdetail` | 客服电话硬编码 `18133233292` | 换号需改代码 |
| 10 | 全页面 | 错误处理均为 `.catch(() => {})` 静默吞错 | 用户无错误反馈 |
---
## 四、Android App 问题(P2)
| # | 问题 |
|---|------|
| 1 | 含大量原家装项目遗留代码(`DesignActivity`、`StyleActivity`、`Douyin` 接口等) |
| 2 | 两套并行网络层:Retrofit(`MyApi.java`)+ OkHttp(`NetApi.java`),架构混乱 |
| 3 | `API.java` 引用已废弃的 Bmob 云后端,含硬编码测试密钥 |
| 4 | 引用 `/device/AppCustomer/*`、`/device/AppDouyin/*` 等可能不存在的接口 |
| 5 | 包名和类名仍保留原产品命名(`kehu`、`find`、`goddess` 等) |
---
## 五、基础设施缺失(P3)
| # | 问题 |
|---|------|
| 1 | 无自动化数据库备份策略 |
| 2 | 无日志聚合 / 监控告警系统 |
| 3 | 自定义建表 SQL(`rlz_order`、`sys_hospital` 等)未入仓 |
| 4 | 无 Staging 测试环境 |
| 5 | CI/CD 仅覆盖后端 Docker 构建,小程序和 Android 无自动化 |
| 6 | 数据库 `rlz_order` 表使用 20 个通用字段 `yuliu1~20`,Schema 不透明 |
---
## 六、数据库问题
| # | 表 | 问题 |
|---|------|------|
| 1 | `rlz_order` | 20 个 `yuliu1`~`yuliu20` 通用 varchar 列,无文档说明 |
| 2 | `rlz_order` / `sys_hospital` / `rlz_service_type` 等 | 建表 DDL 未入仓(仅 `ry_20220625.sql` + `nursing_article.sql` 在 repo 中) |
| 3 | `order_view` / `hospital_user_view` / `user_hospital_view` | 视图 DDL 未入仓 |
| 4 | `sys_user.phonenumber` | 手机号明文存储,无加密 |
| 5 | `sys_oper_log` | 存储完整请求/响应 JSON,含密码和手机号 |
---
## 七、开发工作量估算
### 方案 A:最小可用方案(安全整改 + 关键缺陷修复)— 3~4 个月
| 阶段 | 工作内容 | 人月 |
|------|----------|------|
| 安全整改 | 密钥迁移环境变量、JWT 加固、全接口限流、日志脱敏、Druid/Swagger 生产关闭 | 1.5 |
| 小程序修复 | 退款页/推广页/关于页/机构页修复、硬编码改为接口调用、错误状态完善 | 1.5 |
| 基础设施 | 备份策略、基础监控告警 | 0.5 |
| 全流程测试 | 回归测试 | 0.5 |
| **合计** | | **4 人月** |
> 2 人团队 × 2 个月,费用约 **8~15 万 RMB**
---
### 方案 B:完整商用方案 — 6~8 个月
| 阶段 | 工作内容 | 人月 |
|------|----------|------|
| 安全整改 | 同上 + 敏感数据加密存储 + 渗透测试 + 等保咨询 | 2 |
| 小程序完整修复 | 全部页面真实接口、通知推送、图文混排优化 | 2 |
| 功能补全 | 营销模块(砍价/拼团)、收藏、反馈系统、CMS 完善 | 1.5 |
| Android 重构 | 移除遗留代码、统一网络层、端到端测试 | 2 |
| 基础设施 | CI/CD 全流程、监控告警、日志平台、多环境管理、灾备方案 | 1 |
| 合规 + 文档 | 隐私政策、用户协议、数据删除/导出、运维文档 | 1 |
| 测试 | 全平台回归 + 性能压测 | 1 |
| **合计** | | **10.5 人月** |
> 2~3 人团队 × 4~6 个月,费用约 **25~50 万 RMB**
---
## 八、运维费用估算(月度)
| 项目 | 规格 | 费用(元/月) |
|------|------|---------------|
| 腾讯云 CVM | 4C8G | 300 ~ 600 |
| CynosDB MySQL | 按量 | 500 ~ 2,000 |
| 带宽 + CDN | 按量 | 200 ~ 800 |
| 域名 | ruilaizipj.com | ~10 |
| 微信小程序认证 | 年付 300 | 25 |
| 微信商户手续费 | 0.6% 交易额 | 按交易量 |
| 短信服务 | 验证码 + 通知 | 100 ~ 500 |
| SSL 证书 | Let's Encrypt | 0 |
| 云监控基础版 | - | 200 ~ 500 |
| 对象存储 COS | 图片/文件 | 50 ~ 300 |
| **月度合计** | | **约 1,500 ~ 5,000** |
---
## 九、数据合规要求(中国法律)
陪诊平台属于医疗健康相关服务,涉及用户敏感个人信息,需关注:
| 法规 | 要求 | 当前状态 |
|------|------|----------|
| 《个人信息保护法》 | 收集用户信息需明确告知 + 获取同意 | 小程序无隐私弹窗 |
| 《个人信息保护法》 | 敏感个人信息(医疗健康、行踪轨迹)需单独同意 | 未实现 |
| 《个人信息保护法》 | 用户有权查阅/更正/删除/导出其个人信息 | 未实现 |
| 《网络安全法》 | 网络运营者应采取技术措施保障信息安全 | 多项安全问题未解决 |
| 《数据安全法》 | 重要数据处理者需定期风险评估 | 无 |
| 等保 2.0 | 医疗健康类系统至少等保二级 | 未做 |
| 微信小程序运营规范 | 涉及医疗健康需提供相关资质 | 需确认 |
---
## 十、建议实施路径
```
第 1 个月 ─── 安全整改(密钥管理 + 限流 + 日志脱敏 + 生产配置加固)
第 2 个月 ─── 小程序缺陷修复(假数据 → 真接口 + 空壳页面补全)
第 3 个月 ─── 基础设施(备份 + 监控 + CI/CD)+ 全流程测试
第 4 个月 ─── 上线试运营,收集用户反馈
第 5-6 个月 ─ 功能增强(营销模块、通知推送、数据看板)
第 7-8 个月 ─ Android 重构 + 合规材料准备
```
---
## 十一、总结
| | 方案 A(最小可用) | 方案 B(完整商用) |
|------|------|------|
| 开发周期 | 3 ~ 4 个月 | 6 ~ 8 个月 |
| 开发费用 | 8 ~ 15 万 | 25 ~ 50 万 |
| 月度运维 | 1,500 ~ 5,000 元 | 1,500 ~ 5,000 元 |
| 交付水平 | 核心业务流程可运转 | 具备完整 SaaS 商业化能力 |
**最紧迫事项**:安全漏洞修复(支付密钥、JWT、限流)必须在任何商用推广前完成。源码一旦泄露,攻击者可直接操作微信支付。

View File

@@ -0,0 +1,201 @@
# 瑞来健康 — 域名与 Nginx 配置方案
> 日期:2026-05-24
> 服务器:101.43.95.130
> 现状:与"生长激素预测模型"项目共用服务器和域名 ruilaizipj.com
---
## 一、现状分析
### 1.1 域名占用
```
ruilaizipj.com / www.ruilaizipj.com (80/443)
└── host nginx (/etc/nginx/conf.d/default.conf)
├── / → /www/wwwroot/code/public (ThinkPHP - 生长激素项目)
└── /adminghd/ → ThinkPHP 后台管理
```
### 1.2 RLZ 服务(Docker 内部)
| 服务 | 容器 | 内部端口 | 外部访问 |
|------|------|---------|---------|
| 后端 API | rlz-backend | 8039 | `http://101.43.95.130:8039` |
| 管理后台 | rlz-ui-server | 80 | `http://101.43.95.130:8050` |
| Redis | rlz-redis | 6379 | 127.0.0.1:6379 |
### 1.3 微信小程序约束
微信小程序已在微信后台登记 `https://ruilaizipj.com` 作为合法 API 域名,不能更改。
### 1.4 RLZ API 路径前缀
- 后台管理:`/dev-api/`(由前端 `VUE_APP_BASE_API` 控制)
- 微信小程序/Android:`/prod-api/`(直接请求后端路径)
---
## 二、方案设计
### 2.1 核心思路
**API 走主域名路径代理,管理后台走子域名。**
- 小程序 API → 主域名路径 `/prod-api/*`(不改微信登记,与 ThinkPHP 无冲突)
- 管理后台 → 子域名 `admin.ruilaizipj.com`(SPA 路由清爽,独立 server 块)
### 2.2 目标架构
```
101.43.95.130 (host nginx 80/443)
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
ruilaizipj.com ruilaizipj.com admin.ruilaizipj.com
(生长激素) (RLZ API) (RLZ 管理后台)
│ │ │
/ → code/public /prod-api/* / → rlz-ui-server
/adminghd/ /dev-api/* (127.0.0.1:8050)
│ (127.0.0.1:8039)
▼ │
ThinkPHP 5.0 ▼
(PHP-FPM) rlz-backend
(Spring Boot)
```
### 2.3 SSL 证书
当前 Let's Encrypt 证书覆盖 `ruilaizipj.com` + `www.ruilaizipj.com`。
需要新增 `admin.ruilaizipj.com` 的 SAN 覆盖。两种方式:
- **方式 A**:宝塔面板 → 网站 → SSL → 重新签发(添加子域名)
- **方式 B**:手动 certbot 扩展
推荐方式 A,宝塔自动续期。
---
## 三、实施步骤
### Step 1:DNS 添加子域名
在域名 DNS 管理后台添加记录:
```
admin.ruilaizipj.com → A → 101.43.95.130
```
### Step 2:扩展 SSL 证书
通过宝塔面板重新签发证书,添加 `admin.ruilaizipj.com` 到域名列表。
### Step 3:修改 host nginx 配置
编辑 `/etc/nginx/conf.d/default.conf`,在现有 server 块中新增:
```nginx
# === 现有 ThinkPHP 配置保持不变 ===
# 新增:RLZ 后端 API 代理
location /prod-api/ {
proxy_pass http://127.0.0.1:8039/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_read_timeout 60s;
}
location /dev-api/ {
proxy_pass http://127.0.0.1:8039/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_read_timeout 60s;
}
```
新建 `/etc/nginx/conf.d/rlz-admin.conf`:
```nginx
server {
listen 80;
server_name admin.ruilaizipj.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name admin.ruilaizipj.com;
ssl_certificate /www/server/panel/vhost/cert/101.43.95.130/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/101.43.95.130/privkey.pem;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
ssl_prefer_server_ciphers on;
charset utf-8;
location / {
proxy_pass http://127.0.0.1:8050;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_read_timeout 60s;
}
location /prod-api/ {
proxy_pass http://127.0.0.1:8039/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### Step 4:验证
```bash
# 重载 nginx
nginx -t && nginx -s reload
# 测试 API
curl -s https://ruilaizipj.com/prod-api/captchaImage
# 测试管理后台
curl -s -o /dev/null -w "%{http_code}" https://admin.ruilaizipj.com/
```
### Step 5:更新前端配置
- `rlz-ui/.env.production`:`VUE_APP_BASE_API` 改为 `/prod-api`
- 微信小程序 `config.js`:`baseUrl` 改为 `https://ruilaizipj.com/prod-api`
- Android `BuildConfig`:Release 的 `API_BASE_URL` 改为 `https://ruilaizipj.com/prod-api`
---
## 四、访问地址总览(实施后)
| 端 | 地址 |
|------|------|
| 管理后台 | `https://admin.ruilaizipj.com` |
| 后端 API | `https://ruilaizipj.com/prod-api/` |
| 微信小程序 API | `https://ruilaizipj.com/prod-api/` |
| 生长激素项目(不变) | `https://www.ruilaizipj.com` |
| 生长激素后台(不变) | `https://www.ruilaizipj.com/adminghd/` |
---
## 五、注意事项
1. **不要改 `server_name` 的 ThinkPHP 部分**,否则影响生长激素项目正常运行
2. **nginx 修改前备份**:`cp /etc/nginx/conf.d/default.conf /tmp/default.conf.bak`
3. **宝塔可能覆盖 nginx 配置**,修改后注意观察
4. **API 路径冲突检查**:确认 ThinkPHP 项目没有 `/prod-api/` 或 `/dev-api/` 路由
5. **RLZ 前端的 `VUE_APP_BASE_API` 需要同步更新**为 `/prod-api` 并重新构建部署

View File

@@ -0,0 +1,235 @@
# 服务项目管理动态化方案
> 日期:2026-05-25
> 目标:将小程序端硬编码的服务项目改为后台动态配置,支持上架/下架/调价/排序
---
## 一、现状分析
### 1.1 现有基础设施(已存在,可直接使用)
| 组件 | 状态 | 位置 |
|------|------|------|
| 数据库表 `rlz_service_type` | 已有 | MySQL |
| 实体类 `RlzServiceType` | 已有 | `ruoyi-system/.../domain/RlzServiceType.java` |
| Mapper / Service / Controller | 已有 | CRUD + 分页 + Excel 导出 |
| 价格变更历史 `rlz_price_history` | 已有 | 调价自动记录操作人/时间/原因 |
| 管理后台 Vue 页面 | 已有 | `rlz-ui/src/views/system/serviceType/index.vue` |
### 1.2 现有字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 主键 |
| name | String | 服务名称 |
| category | String | 类别: 1=陪护, 2=陪诊 |
| typeCode | String | 类型编码 |
| price | BigDecimal | 价格 |
| description | String | 服务内容描述 |
| status | String | 0=启用, 1=停用 |
| sortOrder | Integer | 排序(越小越靠前) |
| createBy/createTime | - | 创建信息 |
| updateBy/updateTime | - | 更新信息 |
| remark | String | 备注 |
### 1.3 当前问题
小程序端服务项目全部硬编码:
- `pages/index/index.js:13-41` — 4 个服务项,价格写死(200/20/100/50)
- `pages/peizhen/peizhen.js:26-56` — 陪诊服务列表写死
- `pages/peihu/peihu.js` — 陪护服务列表写死
- 图标通过 `../image/home/{{item.icon}}.png` 本地引用,无法动态配置
---
## 二、改造方案
### 2.1 数据库变更
`rlz_service_type` 表新增 `icon` 字段:
```sql
ALTER TABLE rlz_service_type ADD COLUMN icon VARCHAR(255) DEFAULT '' COMMENT '服务图标URL';
```
### 2.2 后端变更
#### a) 实体新增 icon 字段
`RlzServiceType.java` 增加 `private String icon;` 及 getter/setter。
#### b) Mapper XML 更新
`RlzServiceTypeMapper.xml` 的 resultMap / SQL / insert / update 中增加 icon 字段。
#### c) 新增小程序公开接口
```java
// 无需登录,返回所有启用状态的服务
@GetMapping("/enabled")
public AjaxResult enabled(RlzServiceType rlzServiceType)
{
rlzServiceType.setStatus("0"); // 只查启用的
List<RlzServiceType> list = rlzServiceTypeService
.selectRlzServiceTypeList(rlzServiceType);
return AjaxResult.success(list);
}
```
#### d) SecurityConfig 白名单
```java
.antMatchers("/system/serviceType/enabled").permitAll()
```
### 2.3 管理后台变更
`rlz-ui/src/views/system/serviceType/index.vue`:
- 表格增加"图标"列(显示缩略图)
- 新增/编辑表单增加图标上传组件
- 增加"上架/下架"快捷操作按钮
### 2.4 小程序变更
#### index.js(首页 4 个服务卡片)
```
修改前:data.itemView = [{ icon: "全程陪诊", title: "全程陪诊", price: "200", ... }]
修改后:onShow 调用 /system/serviceType/enabled?category=2 获取陪诊服务列表
```
#### peizhen.js(陪诊页)
```
修改前:services = [{ name: "全程陪诊", price: "200" }, ...]
修改后:调用 /system/serviceType/enabled?category=2 动态加载
```
#### peihu.js(陪护页)
```
修改前:services 硬编码
修改后:调用 /system/serviceType/enabled?category=1 动态加载
```
#### WXML 图标引用
```xml
<!-- 修改前(本地图片) -->
<image src="../image/home/{{item.icon}}.png" mode="aspectFit"></image>
<!-- 修改后(远程URL) -->
<image src="{{item.icon}}" mode="aspectFit"></image>
```
---
## 三、数据流
```
┌─────────────────────────────────────────────────────────┐
│ 管理后台 (Vue Admin) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 服务类型管理 │ │
│ │ [新增] [编辑] [删除] [调价] [上架/下架] │ │
│ │ ┌──────┬──────┬──────┬──────┬──────┐ │ │
│ │ │ 图标 │ 名称 │ 类别 │ 价格 │ 状态 │ │ │
│ │ ├──────┼──────┼──────┼──────┼──────┤ │ │
│ │ │ 🏥 │全程陪诊│陪诊 │ 200 │ 启用 │ │ │
│ │ │ 📋 │诊前约号│陪诊 │ 20 │ 启用 │ │ │
│ │ │ 💊 │代办买药│陪诊 │ 80 │ 停用 │ │ │
│ │ └──────┴──────┴──────┴──────┴──────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ POST/PUT/DELETE │
│ ▼ (需登录 + 权限) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 后端 Controller: /system/serviceType/* │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 微信小程序 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ /system/serviceType/enabled?category=2 │ │
│ │ [公开接口,无需登录] │ │
│ └─────────────────────────────────────────────────┘ │
│ │ GET │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 返回: [{ name:"全程陪诊", price:200, icon:"...", │ │
│ │ description:"排队预约挂号...", status:"0" │ │
│ │ sortOrder:1 }, ...] │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 首页服务卡片区域 动态渲染 │ │
│ │ [全程陪诊] [诊前约号] [代办买药] [取送结果] │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 四、实现步骤
| 步骤 | 内容 | 涉及文件 |
|------|------|----------|
| 1 | 数据库加 `icon` 字段 | DDL |
| 2 | 实体 + Mapper 更新 | `RlzServiceType.java`, `RlzServiceTypeMapper.xml` |
| 3 | Controller 增加 `/enabled` 公开接口 | `RlzServiceTypeController.java` |
| 4 | SecurityConfig 加白名单 | `SecurityConfig.java` |
| 5 | 管理后台增加图标字段 | `serviceType/index.vue` |
| 6 | 小程序首页改为 API 加载 | `index.js`, `index.wxml` |
| 7 | 小程序陪诊页改为 API 加载 | `peizhen.js`, `peizhen.wxml` |
| 8 | 小程序陪护页改为 API 加载 | `peihu.js`, `peihu.wxml` |
| 9 | 部署后端 + 测试 | Docker 构建部署 |
---
## 五、接口设计
### GET /system/serviceType/enabled
无需登录,根据 category 返回已启用的服务列表。
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| category | String | 否 | 1=陪护, 2=陪诊,不传则返回全部 |
**响应示例:**
```json
{
"code": 200,
"data": [
{
"id": 1,
"name": "全程陪诊",
"category": "2",
"typeCode": "1",
"price": 200.00,
"description": "排队预约挂号缴费取药记录医嘱",
"icon": "https://ruilaizipj.com/profile/upload/icon_peizhen.png",
"status": "0",
"sortOrder": 1
},
{
"id": 2,
"name": "诊前约号",
"category": "2",
"typeCode": "2",
"price": 20.00,
"description": "协助办理完成挂号",
"icon": "https://ruilaizipj.com/profile/upload/icon_yuehao.png",
"status": "0",
"sortOrder": 2
}
]
}
```

View File

@@ -0,0 +1,256 @@
# 陪诊员服务费分账方案(A+B 结合)
## 一、需求概述
订单完成后,将服务费按比例分配给平台和陪诊员,实现自动计算 + 人工审核确认的结算流程。
## 二、业务流程
```
订单完成(status=3,待支付)
→ 患者微信支付
→ 支付成功(status=4,已完成)
→ 系统自动计算分账金额(根据配置的抽成比例)
→ 写入结算金额到订单字段
→ 管理员后台审核确认
→ 陪诊员余额增加(yue)、累计收益更新(leijijine)
→ 生成交易记录(jiaoyi_detail)
→ 订单标记为已结算(status=8)
```
## 三、数据库改动
### 3.1 新增平台配置表 `rlz_platform_config`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | bigint | PK |
| `config_key` | varchar(50) | 配置键 |
| `config_value` | varchar(255) | 配置值 |
| `description` | varchar(200) | 描述 |
| `create_time` | datetime | 创建时间 |
| `update_time` | datetime | 更新时间 |
初始化数据:
```sql
INSERT INTO rlz_platform_config (config_key, config_value, description) VALUES
('commission_rate', '20', '平台抽成比例(%)'),
('auto_settle', 'false', '是否自动结算(true/false)');
```
### 3.2 现有表字段利用
`rlz_order` 表已有字段(无需改动):
- `jiesuan_money` — 结算金额(订单总金额)
- `kouchu_money` — 平台抽成金额
- `fanxian_money` — 陪诊员所得金额
- `fanxian_tax` — 抽成比例(%)
- `is_zhifu` — 打款状态(0=未打款, 1=已打款)
`sys_user` 表已有字段(无需改动):
- `yue` — 余额(陪诊员可用余额)
- `leijijine` — 累计收益
`jiaoyi_detail` 表已有字段(无需改动):
- `type` — 1=收入, 2=支出
- `jine` — 金额
- `name` — 交易名称
- `yue` — 交易后余额
- `user_id` — 用户ID
## 四、后端实现
### 4.1 新增类文件
| 类 | 路径 | 职责 |
|----|------|------|
| `RlzPlatformConfig.java` | `domain/` | 平台配置实体 |
| `RlzPlatformConfigMapper.java` | `mapper/` | 配置 Mapper |
| `RlzPlatformConfigMapper.xml` | `mapper/system/` | 配置 SQL |
| `IRlzPlatformConfigService.java` | `service/` | 配置服务接口 |
| `RlzPlatformConfigServiceImpl.java` | `service/impl/` | 配置服务实现 |
| `RlzSettlementService.java` | `service/` | 结算服务(核心逻辑) |
| `RlzSettlementController.java` | `controller/` | 结算接口 |
| `RlzTransactionController.java` | `controller/` | 交易记录查询接口 |
### 4.2 核心服务 RlzSettlementService
```java
@Service
public class RlzSettlementService {
// 获取抽成比例(默认20%)
public BigDecimal getCommissionRate() {
String rate = platformConfigService.getConfigValue("commission_rate", "20");
return new BigDecimal(rate);
}
// 订单支付成功后自动计算分账
public void calculateOrderFee(RlzOrder order) {
BigDecimal totalAmount = new BigDecimal(order.getYuguMoney());
BigDecimal rate = getCommissionRate(); // 20%
BigDecimal kouchu = totalAmount.multiply(rate).divide(new BigDecimal("100")); // 平台抽成
BigDecimal fanxian = totalAmount.subtract(kouchu); // 陪诊员所得
order.setJiesuanMoney(order.getYuguMoney()); // 结算金额=订单金额
order.setKouchuMoney(kouchu.toString()); // 平台抽成
order.setFanxianMoney(fanxian.toString()); // 陪诊员所得
order.setFanxianTax(rate.toString()); // 抽成比例
order.setIsZhifu("0"); // 未打款
rlzOrderService.updateOrder(order);
}
// 管理员确认结算(打款给陪诊员)
@Transactional
public void settleOrder(Long orderId) {
RlzOrder order = rlzOrderService.selectRlzOrderById(orderId);
if (!"4".equals(order.getStatus())) {
throw new BusinessException("订单未支付,无法结算");
}
if ("1".equals(order.getIsZhifu())) {
throw new BusinessException("已打款,请勿重复操作");
}
Long caregiverId = order.getBId(); // 陪诊员ID
BigDecimal fanxianMoney = new BigDecimal(order.getFanxianMoney());
// 1. 更新订单打款状态
order.setIsZhifu("1");
order.setStatus("8"); // 已结算
rlzOrderService.updateOrder(order);
// 2. 更新陪诊员余额和累计收益
SysUser caregiver = sysUserService.selectUserById(caregiverId);
BigDecimal oldBalance = new BigDecimal(caregiver.getYue() != null ? caregiver.getYue() : "0");
BigDecimal newBalance = oldBalance.add(fanxianMoney);
caregiver.setYue(newBalance.toString());
BigDecimal oldTotal = new BigDecimal(caregiver.getLeijijine() != null ? caregiver.getLeijijine() : "0");
caregiver.setLeijijine(oldTotal.add(fanxianMoney).toString());
sysUserService.updateUser(caregiver);
// 3. 生成交易记录
insertTransaction(caregiverId, fanxianMoney, newBalance, order.getYuliu1());
}
private void insertTransaction(Long userId, BigDecimal jine, BigDecimal newBalance, String orderNo) {
JiaoyiDetail detail = new JiaoyiDetail();
detail.setUserId(userId);
detail.setType("1"); // 收入
detail.setJine(jine.toString());
detail.setYue(newBalance.toString());
detail.setName("订单结算 - " + orderNo);
jiaoyiDetailService.insertJiaoyiDetail(detail);
}
}
```
### 4.3 结算接口 RlzSettlementController
```java
@RestController
@RequestMapping("/system/settlement")
public class RlzSettlementController extends BaseController {
@Autowired
private RlzSettlementService settlementService;
// 支付回调中自动触发计算(在 WxPayNotify 中调用)
// 此处提供手动重新计算的接口
// 管理员确认结算
@PostMapping("/settle/{orderId}")
public AjaxResult settle(@PathVariable Long orderId) {
settlementService.settleOrder(orderId);
return AjaxResult.success("结算完成");
}
// 批量结算
@PostMapping("/batchSettle")
public AjaxResult batchSettle(@RequestBody Long[] orderIds) {
for (Long id : orderIds) {
settlementService.settleOrder(id);
}
return AjaxResult.success("批量结算完成");
}
// 查询待结算订单列表(status=4 已支付但未打款)
@GetMapping("/pending")
public TableDataInfo pending() {
// 查询 status=4 AND is_zhifu='0' 的订单
...
}
}
```
### 4.4 支付回调中自动触发计算
在 `WxPayNotify` 处理中增加一行调用:
```java
// 支付成功,更新 status=4
order.setStatus("4");
// ... 存储微信交易号 ...
rlzOrderService.updateOrder(order);
// === 新增:自动计算分账金额 ===
settlementService.calculateOrderFee(order);
```
## 五、后台管理面板改动
### 5.1 新增菜单
| 父菜单 | 菜单名称 | 路径 | 组件 |
|--------|----------|------|------|
| 财务管理(3000) | 结算管理 | `settlement` | `finance/settlement/index` |
| 财务管理(3000) | 交易明细 | `transaction` | `finance/transaction/index` |
| 财务管理(3000) | 平台配置 | `platformConfig` | `finance/platformConfig/index` |
### 5.2 结算管理页面 `finance/settlement/index.vue`
功能:
- 列表展示已支付待结算订单
- 搜索:订单号、陪诊员姓名、日期范围
- 列:订单号、陪诊员、订单金额、平台抽成、应发金额、支付时间、状态
- 操作:单个结算 / 批量结算
- 点击结算弹出确认框,显示分账明细预览
### 5.3 平台配置页面 `finance/platformConfig/index.vue`
功能:
- 平台抽成比例设置(默认20%)
- 自动结算开关(暂不启用)
## 六、SecurityConfig 更新
```java
// 新增匿名访问(若小程序也需要查询余额/交易记录)
// 结算接口需要认证(仅后台管理员操作)
.antMatchers("/system/settlement/**", "/system/transaction/**").authenticated()
```
## 七、实施步骤
| 步骤 | 内容 | 预估 |
|------|------|------|
| 1 | 创建 `rlz_platform_config` 表 + 初始化数据 | 10min |
| 2 | 创建配置管理后端代码(CRUD) | 20min |
| 3 | 实现 `RlzSettlementService` 核心分账逻辑 | 30min |
| 4 | 在支付回调中集成自动计算 | 10min |
| 5 | 创建结算管理后台页面 | 30min |
| 6 | 创建平台配置后台页面 | 20min |
| 7 | 部署、测试完整流程 | 20min |
## 八、分账示例
假设平台抽成比例 20%,订单金额 500 元:
| 项目 | 金额 | 计算 |
|------|------|------|
| 订单金额 | 500.00 | 患者支付 |
| 平台抽成(20%) | 100.00 | 500 × 20% |
| 陪诊员所得 | 400.00 | 500 × 80% |
| 陪诊员余额变化 | +400.00 | yue += 400 |
管理员可在后台"平台配置"中调整抽成比例,新比例对后续订单生效。

View File

@@ -14,14 +14,6 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true //突破应用方法数65535的一个限制
}
signingConfigs {
release {
storeFile file('july.jks')
storePassword "july"
keyAlias "123456"
keyPassword "123456"
}
}
signingConfigs {
release {
storeFile file('ruilaizi.jks')
@@ -39,7 +31,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
// 生产环境 —— 连接公网服务器
// 生产环境 —— 通过域名访问
buildConfigField "String", "API_BASE_URL", '"http://101.43.95.130:8039"'
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

View File

@@ -35,6 +35,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application
android:name=".BaseApplication"
@@ -45,6 +46,13 @@
android:roundIcon="@mipmap/fw_logo"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- 微信支付回调Activity -->
<activity
android:name=".wxapi.WXPayEntryActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity android:name=".main.my.activity.ShimingActivity"
android:windowSoftInputMode="adjustPan"
>

View File

@@ -116,14 +116,14 @@ public class BaseApplication extends Application {
//https相关设置,以下几种方案根据需要自己设置
//方法一:信任所有证书,不安全有风险
HttpsUtils.SSLParams sslParams1 = HttpsUtils.getSslSocketFactory();
//HttpsUtils.SSLParams sslParams1 = HttpsUtils.getSslSocketFactory();
//方法二:自定义信任规则,校验服务端证书
HttpsUtils.SSLParams sslParams2 = HttpsUtils.getSslSocketFactory(new SafeTrustManager());
//方法三:使用预埋证书,校验服务端证书(自签名证书)
//HttpsUtils.SSLParams sslParams3 = HttpsUtils.getSslSocketFactory(getAssets().open("srca.cer"));
//方法四:使用bks证书和密码管理客户端证书(双向认证),使用预埋证书,校验服务端证书(自签名证书)
//HttpsUtils.SSLParams sslParams4 = HttpsUtils.getSslSocketFactory(getAssets().open("xxx.bks"), "123456", getAssets().open("yyy.cer"));
builder.sslSocketFactory(sslParams1.sSLSocketFactory, sslParams1.trustManager);
builder.sslSocketFactory(sslParams2.sSLSocketFactory, sslParams2.trustManager);
//配置https的域名匹配规则,详细看demo的初始化介绍,不需要就不要加入,使用不当会导致https握手失败
builder.hostnameVerifier(new SafeHostnameVerifier());

View File

@@ -9,6 +9,7 @@ import android.view.WindowManager;
import com.ruilaizi.service.base.BaseActivity;
import com.ruilaizi.service.utils.ToastUtils;
import com.ruilaizi.service.utils.checkVersionsUtils.ProfileSpUtils;
import java.lang.ref.WeakReference;
import java.util.List;
@@ -69,13 +70,11 @@ public class SplashActivity extends BaseActivity implements EasyPermissions.Perm
private void startActivity() {
if (ProfileSpUtils.getInstance().isLogin()) {
startActivity(MainActivity.class);
//此类逻辑有是否登录,没有登录则跳验证码登录
// if (ProfileSpUtils.getInstance().isLogin()) {
// startActivity(MainActivity.class);
// } else {
// startActivity(LoginPasswordActivity.class);
// }
} else {
startActivity(LoginPasswordActivity.class);
}
finish();
}
@@ -88,9 +87,13 @@ public class SplashActivity extends BaseActivity implements EasyPermissions.Perm
@AfterPermissionGranted(RC_CAMERA_AND_LOCATION)
private void methodRequiresTwoPermission() {
String[] perms = {Manifest.permission.WRITE_EXTERNAL_STORAGE
, Manifest.permission.CAMERA
};
String[] perms;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
// Android 13+: WRITE_EXTERNAL_STORAGE 已废弃
perms = new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_MEDIA_IMAGES};
} else {
perms = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA};
}
if (EasyPermissions.hasPermissions(this, perms)) {
handler.sendEmptyMessageDelayed(0, 2000);

View File

@@ -3,6 +3,7 @@ package com.ruilaizi.service.main.activity.mvp.presenterImpl;
import android.text.TextUtils;
import android.util.Log;
import com.lzy.okgo.OkGo;
import com.lzy.okgo.model.HttpParams;
import com.lzy.okgo.model.Response;
import com.ruilaizi.service.main.activity.MainActivity;
@@ -77,9 +78,14 @@ public class LoginPasswordPresenterImpl extends MvpBasePresenter<LoginPasswordCo
}
getView().showProgressDialog("登录中...");
// 清除旧的全局 Authorization header,否则 Spring Security 会拒绝登录请求
com.lzy.okgo.model.HttpHeaders commonHeaders = OkGo.getInstance().getCommonHeaders();
if (commonHeaders != null) {
commonHeaders.remove("Authorization");
}
final HttpParams paramsPost = new HttpParams();
paramsPost.put("username", "admin");
paramsPost.put("password", "123456");
paramsPost.put("username", mobile);
paramsPost.put("password", passWord);
new NetApi().getPostData(paramsPost, HttpConstants.URi_appLogin).subscribe(new Observer<Response>() {
@Override
public void onNext(Response response) {

View File

@@ -13,6 +13,7 @@ public class kehuListBean extends BaseModel {
private int code;
private String msg;
private List<KehuinfoBean> data;
private List<KehuinfoBean> rows;
public int getTotal() {
return total;
@@ -39,13 +40,21 @@ public class kehuListBean extends BaseModel {
}
public List<KehuinfoBean> getData() {
return data;
return rows != null ? rows : data;
}
public void setData(List<KehuinfoBean> data) {
this.data = data;
}
public List<KehuinfoBean> getRows() {
return rows;
}
public void setRows(List<KehuinfoBean> rows) {
this.rows = rows;
}
// public static class DataBean extends BaseObservable implements Serializable {
// private Object searchValue;
// private Object createBy;

View File

@@ -231,7 +231,7 @@ public class CanFragment extends MobanFragment {
public void onNext(Response response) {
String body = (String) response.body();
kehuListBean data = JsonUtils.fromJson(body, kehuListBean.class);
if (data.getCode() == 200) {
if (data.getCode() == 200 || data.getCode() == 0) {
List<KehuinfoBean> data1 = data.getData();
mSwl.setRefreshing(false);

View File

@@ -232,7 +232,7 @@ public class ComFragment extends MobanFragment {
public void onNext(Response response) {
String body = (String) response.body();
kehuListBean data = JsonUtils.fromJson(body, kehuListBean.class);
if (data.getCode() == 200) {
if (data.getCode() == 200 || data.getCode() == 0) {
List<KehuinfoBean> data1 = data.getData();
mSwl.setRefreshing(false);

View File

@@ -231,7 +231,7 @@ public class CustomerFragment extends MobanFragment {
public void onNext(Response response) {
String body = (String) response.body();
kehuListBean data = JsonUtils.fromJson(body, kehuListBean.class);
if (data.getCode() == 200) {
if (data.getCode() == 200 || data.getCode() == 0) {
List<KehuinfoBean> data1 = data.getData();
mSwl.setRefreshing(false);

View File

@@ -232,7 +232,7 @@ public class SerFragment extends MobanFragment {
public void onNext(Response response) {
String body = (String) response.body();
kehuListBean data = JsonUtils.fromJson(body, kehuListBean.class);
if (data.getCode() == 200) {
if (data.getCode() == 200 || data.getCode() == 0) {
List<KehuinfoBean> data1 = data.getData();
mSwl.setRefreshing(false);

View File

@@ -244,7 +244,7 @@ public class TobeSerFragment extends MobanFragment {
public void onNext(Response response) {
String body = (String) response.body();
kehuListBean data = JsonUtils.fromJson(body, kehuListBean.class);
if (data.getCode() == 200) {
if (data.getCode() == 200 || data.getCode() == 0) {
List<KehuinfoBean> data1 = data.getData();
mSwl.setRefreshing(false);

View File

@@ -37,6 +37,10 @@ import com.zhihu.matisse.MimeType;
import com.zhihu.matisse.engine.impl.PicassoEngine;
import com.zhihu.matisse.internal.entity.CaptureStrategy;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -123,7 +127,7 @@ public class ShimingActivity extends BaseActivity {
toast("请输入姓名");
return;
}
if("".equals(recFollowBean.getRealName())){
if("".equals(recFollowBean.getIdentityNo())){
toast("请输入身份证号");
return;
}
@@ -197,10 +201,43 @@ public class ShimingActivity extends BaseActivity {
}
/**
* 将图片文件压缩后转为 base64,限制最大宽度并控制 JPEG 质量
*/
private String compressAndToBase64(File file, int maxWidth) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int sampleSize = 1;
int width = options.outWidth;
if (width > 0) {
while (width / sampleSize > maxWidth) {
sampleSize *= 2;
}
}
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bitmap == null) {
return "";
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos);
bitmap.recycle();
return "data:image/jpg;base64," + android.util.Base64.encodeToString(baos.toByteArray(), android.util.Base64.NO_WRAP);
}
private void getPermissions() {
RxPermissions rxPermissions = new RxPermissions(this);
rxPermissions.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)
String[] permissions;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
// Android 13+: WRITE_EXTERNAL_STORAGE 已废弃,需用 READ_MEDIA_IMAGES
permissions = new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_MEDIA_IMAGES};
} else {
permissions = new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE};
}
rxPermissions.request(permissions)
.subscribe(new io.reactivex.Observer<Boolean>() {
@Override
public void onSubscribe(Disposable d) {
@@ -287,10 +324,9 @@ public class ShimingActivity extends BaseActivity {
@Override
public void onSuccess(File file) {
//转换成功后的文件
String resultt = AbImageUtil.bitmapToBase64(ImageUtils.getBitmap(file));
uploadPic(resultt,0);
showProgressDialog("上传中...", ShimingActivity.this);
String resultt = compressAndToBase64(file, 600);
uploadPic(resultt, 0);
}
@Override
@@ -327,10 +363,9 @@ public class ShimingActivity extends BaseActivity {
@Override
public void onSuccess(File file) {
//转换成功后的文件
String resultt = AbImageUtil.bitmapToBase64(ImageUtils.getBitmap(file));
uploadPic(resultt,1);
showProgressDialog("上传中...", ShimingActivity.this);
String resultt = compressAndToBase64(file, 1200);
uploadPic(resultt, 1);
}
@Override
@@ -367,9 +402,9 @@ public class ShimingActivity extends BaseActivity {
@Override
public void onSuccess(File file) {
//转换成功后的文件
String resultt = AbImageUtil.bitmapToBase64(ImageUtils.getBitmap(file));
uploadPic(resultt,2);
showProgressDialog("上传中...", ShimingActivity.this);
String resultt = compressAndToBase64(file, 1200);
uploadPic(resultt, 2);
}
@Override

View File

@@ -105,6 +105,8 @@ public class loginBean implements Serializable {
private String leijijine;
private String yue;
private String token;
private String monthIncome;
private String monthOrderCount;
public String getUserId() {
return userId;
@@ -169,5 +171,21 @@ public class loginBean implements Serializable {
public void setToken(String token) {
this.token = token;
}
public String getMonthIncome() {
return monthIncome;
}
public void setMonthIncome(String monthIncome) {
this.monthIncome = monthIncome;
}
public String getMonthOrderCount() {
return monthOrderCount;
}
public void setMonthOrderCount(String monthOrderCount) {
this.monthOrderCount = monthOrderCount;
}
}
}

View File

@@ -27,8 +27,8 @@ public class ViewHolderOne extends AbstractViewTypeHolder {
@Override
public void bindHolder(robCustomerInfoBean dataBean, int position, Context mContext, MarKQuestionItemviewOnClickListener mviewListener) {
super.bindHolder(dataBean, position, mContext, mviewListener);
WidgetTools.setTextsev(my_shouru, "", ProfileSpUtils.getInstance().getUserProfie().getData().getYue());
WidgetTools.setTextsev(my_dingdan_num, "", ProfileSpUtils.getInstance().getUserProfie().getData().getYue());
WidgetTools.setTextsev(my_shouru, "", ProfileSpUtils.getInstance().getUserProfie().getData().getMonthIncome());
WidgetTools.setTextsev(my_dingdan_num, "", ProfileSpUtils.getInstance().getUserProfie().getData().getMonthOrderCount());
}
}

View File

@@ -10,6 +10,7 @@ import com.tencent.mm.opensdk.modelbase.BaseReq;
import com.tencent.mm.opensdk.modelbase.BaseResp;
import com.tencent.mm.opensdk.openapi.IWXAPI;
import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler;
import com.tencent.mm.opensdk.openapi.WXAPIFactory;
import androidx.annotation.Nullable;
@@ -57,11 +58,10 @@ public class WXPayEntryActivity extends BaseActivity implements IWXAPIEventHandl
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initData();
}
private void initData() {
// api = WXAPIFactory.createWXAPI(this, PayUtils.APP_ID);
api = WXAPIFactory.createWXAPI(this, WXEntryActivity.WEIXIN_APP_ID, true);
api.handleIntent(getIntent(), this);
}

BIN
rlz-ui-dist-new.tar.gz Normal file

Binary file not shown.

View File

@@ -0,0 +1,70 @@
import request from '@/utils/request'
// 查询待结算订单列表
export function listPendingSettlement(query) {
return request({
url: '/system/settlement/pending',
method: 'get',
params: query
})
}
// 单个结算
export function settleOrder(orderId) {
return request({
url: '/system/settlement/settle/' + orderId,
method: 'post'
})
}
// 批量结算
export function batchSettleOrder(orderIds) {
return request({
url: '/system/settlement/batchSettle',
method: 'post',
data: orderIds
})
}
// 查询平台配置列表
export function listPlatformConfig(query) {
return request({
url: '/system/platformConfig/list',
method: 'get',
params: query
})
}
// 获取平台配置详情
export function getPlatformConfig(id) {
return request({
url: '/system/platformConfig/' + id,
method: 'get'
})
}
// 新增平台配置
export function addPlatformConfig(data) {
return request({
url: '/system/platformConfig',
method: 'post',
data: data
})
}
// 修改平台配置
export function updatePlatformConfig(data) {
return request({
url: '/system/platformConfig',
method: 'put',
data: data
})
}
// 删除平台配置
export function delPlatformConfig(id) {
return request({
url: '/system/platformConfig/' + id,
method: 'delete'
})
}

View File

@@ -0,0 +1,146 @@
<template>
<div class="app-container">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="配置键" align="center" prop="configKey" width="180" />
<el-table-column label="配置值" align="center" prop="configValue" width="180">
<template slot-scope="scope">
<el-tag type="warning" size="small">{{ scope.row.configValue }}</el-tag>
</template>
</el-table-column>
<el-table-column label="描述" align="center" prop="description" min-width="200" show-overflow-tooltip />
<el-table-column label="更新时间" align="center" prop="updateTime" width="160">
<template slot-scope="scope">
{{ scope.row.updateTime || scope.row.createTime || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="配置键" prop="configKey">
<el-input v-model="form.configKey" placeholder="请输入配置键" :disabled="isEdit" />
</el-form-item>
<el-form-item label="配置值" prop="configValue">
<el-input v-model="form.configValue" placeholder="请输入配置值" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="请输入描述" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogOpen = false">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listPlatformConfig, getPlatformConfig, addPlatformConfig, updatePlatformConfig, delPlatformConfig } from "@/api/finance/settlement";
export default {
name: "PlatformConfig",
data() {
return {
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
configList: [],
dialogOpen: false,
dialogTitle: "",
isEdit: false,
form: {},
rules: {
configKey: [{ required: true, message: "配置键不能为空", trigger: "blur" }],
configValue: [{ required: true, message: "配置值不能为空", trigger: "blur" }],
},
};
},
created() {
this.getList();
},
methods: {
getList() {
this.loading = true;
listPlatformConfig({}).then(response => {
this.configList = response.rows || response.data || [];
this.loading = false;
});
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
reset() {
this.form = {};
},
handleAdd() {
this.reset();
this.dialogTitle = "新增配置";
this.isEdit = false;
this.dialogOpen = true;
},
handleUpdate(row) {
this.reset();
this.dialogTitle = "修改配置";
this.isEdit = true;
getPlatformConfig(row.id).then(response => {
this.form = response.data;
this.dialogOpen = true;
});
},
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.isEdit) {
updatePlatformConfig(this.form).then(() => {
this.$modal.msgSuccess("修改成功");
this.dialogOpen = false;
this.getList();
});
} else {
addPlatformConfig(this.form).then(() => {
this.$modal.msgSuccess("新增成功");
this.dialogOpen = false;
this.getList();
});
}
}
});
},
handleDelete(row) {
const id = row.id || this.ids[0];
if (!id) {
this.$modal.msgWarning("请选择要删除的配置");
return;
}
this.$modal.confirm('确认删除该配置?').then(() => {
return delPlatformConfig(id);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
}
};
</script>

View File

@@ -1,18 +1,8 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="订单编号" prop="orderNumber">
<el-input v-model="queryParams.orderNumber" placeholder="请输入订单编号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="陪护姓名" prop="userbName">
<el-input v-model="queryParams.userbName" placeholder="请输入陪护姓名" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="结算状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="全部" :value="null" />
<el-option label="待结算" value="4" />
<el-option label="已结算" value="8" />
</el-select>
<el-form-item label="订单编号" prop="yuliu1">
<el-input v-model="queryParams.yuliu1" placeholder="请输入订单编号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@@ -22,87 +12,56 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
<el-button type="warning" plain icon="el-icon-money" size="mini" :disabled="multiple" @click="handleBatchSettle">批量结算</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="orderList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="订单编号" align="center" prop="orderNumber" min-width="160" />
<el-table-column label="患者" align="center" prop="usercName" width="100" />
<el-table-column label="陪护" align="center" prop="userbName" width="100" />
<el-table-column label="医院" align="center" prop="hospitalName" min-width="120" show-overflow-tooltip />
<el-table-column label="结算金额" align="center" prop="jiesuanMoney" width="110">
<el-table-column label="订单编号" align="center" prop="yuliu1" width="180" :show-overflow-tooltip="true" />
<el-table-column label="支付时间" align="center" prop="updateTime" width="160" />
<el-table-column label="订单金额" align="center" width="120">
<template slot-scope="scope">
{{ scope.row.jiesuanMoney || scope.row.yuguMoney || '-' }}
<span style="color: #E6A23C; font-weight: bold;">¥{{ scope.row.jiesuanMoney || scope.row.yuguMoney || '0' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="90">
<el-table-column label="平台抽成" align="center" width="120">
<template slot-scope="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small">
{{ statusMap[scope.row.status] || scope.row.status }}
</el-tag>
<span v-if="scope.row.kouchuMoney" style="color: #F56C6C;">¥{{ scope.row.kouchuMoney }}</span>
<span v-else style="color: #C0C4CC;">待计算</span>
</template>
</el-table-column>
<el-table-column label="完成时间" align="center" prop="endTime" width="160" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
<el-table-column label="抽成比例" align="center" prop="fanxianTax" width="80">
<template slot-scope="scope">
<el-button size="mini" type="primary" icon="el-icon-bank-card" @click="handleSettle(scope.row)"
v-if="scope.row.status == '4'">结算</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)">详情</el-button>
{{ scope.row.fanxianTax ? scope.row.fanxianTax + '%' : '-' }}
</template>
</el-table-column>
<el-table-column label="应发金额" align="center" width="120">
<template slot-scope="scope">
<span style="color: #67C23A; font-weight: bold;">¥{{ scope.row.fanxianMoney || '0' }}</span>
</template>
</el-table-column>
<el-table-column label="支付状态" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.isZhifu === '1'" type="success" size="small">已结算</el-tag>
<el-tag v-else type="warning" size="small">待结算</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
<template slot-scope="scope">
<el-button v-if="scope.row.isZhifu !== '1'" size="mini" type="primary" @click="handleSettle(scope.row)">结算</el-button>
<span v-else style="color: #67C23A;">已打款</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
<!-- 结算对话框 -->
<el-dialog title="订单结算" :visible.sync="dialogOpen" width="450px" append-to-body>
<el-form ref="settleForm" :model="settleForm" :rules="settleRules" label-width="100px">
<el-form-item label="订单编号">
<span>{{ settleForm.orderNumber }}</span>
</el-form-item>
<el-form-item label="陪护姓名">
<span>{{ settleForm.userbName }}</span>
</el-form-item>
<el-form-item label="结算金额" prop="jiesuanMoney">
<el-input v-model="settleForm.jiesuanMoney" placeholder="请输入结算金额">
<template slot="append">元</template>
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitSettle">确认结算</el-button>
<el-button @click="dialogOpen = false">取 消</el-button>
</div>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="订单详情" :visible.sync="detailOpen" width="650px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="订单编号">{{ detail.orderNumber }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusTagType(detail.status)" size="small">
{{ statusMap[detail.status] || detail.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="患者">{{ detail.usercName || '-' }}</el-descriptions-item>
<el-descriptions-item label="陪护">{{ detail.userbName || '-' }}</el-descriptions-item>
<el-descriptions-item label="医院">{{ detail.hospitalName || '-' }}</el-descriptions-item>
<el-descriptions-item label="结算金额">{{ detail.jiesuanMoney || detail.yuguMoney || '-' }}</el-descriptions-item>
<el-descriptions-item label="扣费">{{ detail.kouchuMoney || '-' }}</el-descriptions-item>
<el-descriptions-item label="返现">{{ detail.fanxianMoney || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ detail.createTime }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</div>
</template>
<script>
import { listSettlementOrders, settleOrder } from "@/api/system/finance";
import { getOrderInfo } from "@/api/system/order";
import { listPendingSettlement, settleOrder, batchSettleOrder } from "@/api/finance/settlement";
export default {
name: "Settlement",
@@ -110,34 +69,25 @@ export default {
return {
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
orderList: [],
dialogOpen: false,
detailOpen: false,
detail: {},
settleForm: { orderId: null, orderNumber: '', userbName: '', jiesuanMoney: '' },
settleRules: {
jiesuanMoney: [{ required: true, message: '请输入结算金额', trigger: 'blur' }]
},
statusMap: { '4': '已完成', '8': '已结算' },
queryParams: {
pageNum: 1, pageSize: 10,
orderNumber: null, userbName: null, status: null,
}
pageNum: 1,
pageSize: 10,
yuliu1: null,
},
};
},
created() {
this.getList();
},
methods: {
statusTagType(status) {
const map = { '4': 'success', '8': 'info' };
return map[status] || '';
},
getList() {
this.loading = true;
listSettlementOrders(this.queryParams).then(response => {
listPendingSettlement(this.queryParams).then(response => {
this.orderList = response.data;
this.total = response.total;
this.loading = false;
@@ -149,42 +99,34 @@ export default {
},
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
this.queryParams.pageNum = 1;
this.getList();
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.orderId);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
handleSettle(row) {
this.settleForm = {
orderId: row.orderId,
orderNumber: row.orderNumber,
userbName: row.userbName,
jiesuanMoney: row.jiesuanMoney || row.yuguMoney || ''
};
this.dialogOpen = true;
},
submitSettle() {
this.$refs["settleForm"].validate(valid => {
if (valid) {
settleOrder(this.settleForm.orderId, {
jiesuanMoney: this.settleForm.jiesuanMoney
const orderId = row.orderId;
this.$modal.confirm('确认结算该订单?将向陪诊员打款 ¥' + (row.fanxianMoney || '0')).then(() => {
return settleOrder(orderId);
}).then(() => {
this.$modal.msgSuccess("结算成功");
this.dialogOpen = false;
this.getList();
});
this.$modal.msgSuccess("结算完成");
}).catch(() => {});
},
handleBatchSettle() {
if (this.ids.length === 0) {
this.$modal.msgWarning("请选择订单");
return;
}
});
},
handleDetail(row) {
getOrderInfo(row.orderId).then(response => {
this.detail = response.data;
this.detailOpen = true;
});
},
handleExport() {
this.download('system/view/export', { ...this.queryParams },
`settlement_${new Date().getTime()}.xlsx`)
this.$modal.confirm('确认批量结算 ' + this.ids.length + ' 个订单?').then(() => {
return batchSettleOrder(this.ids);
}).then(() => {
this.getList();
this.$modal.msgSuccess("批量结算完成");
}).catch(() => {});
},
}
};

View File

@@ -94,6 +94,12 @@
<span style="color: #f56c6c; font-weight: bold;">{{ scope.row.price }}</span>
</template>
</el-table-column>
<el-table-column label="图标" align="center" prop="icon" width="80">
<template slot-scope="scope">
<img v-if="scope.row.icon" :src="scope.row.icon" style="width:40px;height:40px;border-radius:4px;" />
<span v-else style="color:#ccc;">-</span>
</template>
</el-table-column>
<el-table-column label="服务描述" align="center" prop="description" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status" width="80">
<template slot-scope="scope">
@@ -170,6 +176,9 @@
<el-form-item label="服务描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入服务内容描述" />
</el-form-item>
<el-form-item label="图标URL" prop="icon">
<el-input v-model="form.icon" placeholder="请输入图标URL地址" />
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" style="width: 100%" placeholder="请输入排序号" />
</el-form-item>
@@ -315,6 +324,7 @@ export default {
typeCode: null,
price: null,
description: null,
icon: null,
status: '0',
sortOrder: 0,
remark: null,

View File

@@ -46,6 +46,8 @@ public class SysLoginController
private ISysUserService userService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private com.ruoyi.system.service.IRlzOrderService rlzOrderService;
/**
* 登录方法
@@ -67,18 +69,33 @@ public class SysLoginController
public AjaxResult appLogin(LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.appLogin(loginBody.getUsername(), loginBody.getPassword());
JSONObject data=new JSONObject();
data.put ("userId","1");
data.put ("userName","admin");
data.put ("nickName","管理员");
data.put ("phonenumber","13333333333");
data.put("headimage","/profile/avatar/2022/07/08/blob_20220708142648A004.jpeg");
data.put ("leijijine",52345.20);//累计收益
data.put ("yue",3223.00);
data.put (Constants.TOKEN,token);
ajax.put(Constants.REQUST_DATA,data);
// 生成令牌并获取真实用户信息
com.ruoyi.common.core.domain.model.LoginUser loginUser = loginService.appLoginWithUser(loginBody.getUsername(), loginBody.getPassword());
SysUser user = loginUser.getUser();
String token = loginUser.getToken();
JSONObject data = new JSONObject();
if (user != null) {
data.put("userId", user.getUserId());
data.put("userName", user.getUserName());
data.put("nickName", user.getNickName() != null ? user.getNickName() : "");
data.put("phonenumber", user.getPhonenumber() != null ? user.getPhonenumber() : "");
data.put("headimage", user.getAvatar() != null ? user.getAvatar() : "");
data.put("leijijine", user.getLeijijine() != null ? user.getLeijijine() : "0");
data.put("yue", user.getYue() != null ? user.getYue() : "0");
// 查询本月收入和本月接单
try {
java.util.Map<String, Object> stats = rlzOrderService.getCaregiverMonthlyStats(user.getUserId());
Object income = stats.get("income");
Object orderCount = stats.get("orderCount");
data.put("monthIncome", income != null ? income.toString() : "0");
data.put("monthOrderCount", orderCount != null ? orderCount.toString() : "0");
} catch (Exception e) {
data.put("monthIncome", "0");
data.put("monthOrderCount", "0");
}
}
data.put(Constants.TOKEN, token);
ajax.put(Constants.REQUST_DATA, data);
return ajax;
}
@PostMapping("/smsLogin")
@@ -103,7 +120,7 @@ public class SysLoginController
u.setUserName(phoneNumber);
u.setPassword(SecurityUtils.encryptPassword("123456"));
u.setNickName("游客");
u.setAvatar("http://81.68.125.247:9000/rlz/2023-05-28/aa17ed44-8fc9-4e00-a419-862b18bc74b2.jpg");
u.setAvatar("http://101.43.95.130:9000/rlz/2023-05-28/aa17ed44-8fc9-4e00-a419-862b18bc74b2.jpg");
userService.insertUser(u);
}
// 生成令牌

View File

@@ -376,8 +376,9 @@ public class SysUserController extends BaseController
public AjaxResult getAppIndexInfo(){
AjaxResult ajax = AjaxResult.success();
//app获取个人信息
// LoginUser loginUser = getLoginUser();
// SysUser user= loginUser.getUser();
LoginUser loginUser = getLoginUser();
SysUser user = loginUser.getUser();
Long userId = user.getUserId();
JSONObject data=new JSONObject();
JSONObject receiveOrder=new JSONObject();//新消息, 新订单是orderId和orderCreteTime,服务中 name,phonenumber,headimage
JSONArray realTimeInfoList= new JSONArray(); //护理咨询
@@ -392,13 +393,17 @@ public class SysUserController extends BaseController
realTimeInfoList.add(item);
}
}
data.put("monthOrderCount",21);//本月接单数量
// 查询本月收入和本月接单
java.util.Map<String, Object> stats = rlzOrderService.getCaregiverMonthlyStats(userId);
Object monthIncome = stats.get("income");
Object monthOrderCount = stats.get("orderCount");
data.put("monthOrderCount", monthOrderCount != null ? monthOrderCount : 0);
data.put("liaojiepeizhen","http://ruilaizipj.com/ljpz.html");//了解陪诊
data.put("fuwubiaozhun","http://ruilaizipj.com/fwbz.html");//服务标准
data.put("feiyongbiaozhun","http://ruilaizipj.com/fybz.html");//费用标准
data.put("yinsizhengce","http://ruilaizipj.com/yszc.html");//费用标准
data.put("monthincome",9400);//本月收入
data.put("monthincome", monthIncome != null ? monthIncome : 0);//本月收入
data.put("realTimeInfoList",realTimeInfoList);//护理咨询
ajax.put(Constants.REQUST_DATA,data);
return ajax;

View File

@@ -109,8 +109,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login","/appLogin","/system/msm","/smsLogin", "/register", "/captchaImage","/weixinLogin","/getUserToken","/system/weixin/wxPayNotify","/system/weixin/wxRefundNotify","/system/user/getAppIndexInfo").anonymous()
.antMatchers("/system/nursing/published","/system/nursing/view/**").permitAll()
.antMatchers("/login","/appLogin","/system/msm","/smsLogin", "/register", "/captchaImage","/weixinLogin","/getUserToken","/system/weixin/wxPayNotify","/system/weixin/wxRefundNotify").anonymous()
.antMatchers("/system/user/getAppIndexInfo","/system/hospital/list","/system/config/configKey/**","/system/nursing/published","/system/nursing/view/**","/system/serviceType/enabled").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()

View File

@@ -126,6 +126,39 @@ public class SysLoginService
return tokenService.createToken(loginUser);
}
/**
* APP登录并返回LoginUser对象
*/
public LoginUser appLoginWithUser(String username, String password)
{
Authentication authentication = null;
try
{
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// createToken sets UUID on loginUser and returns JWT, we need JWT for client
String jwtToken = tokenService.createToken(loginUser);
loginUser.setToken(jwtToken);
return loginUser;
}
/**
* 校验验证码
*

View File

@@ -8,6 +8,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.domain.RlzOrder;
@@ -31,6 +33,9 @@ import com.ruoyi.system.domain.OrderView;
import com.ruoyi.system.service.IOrderViewService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.util.WXPayConstants;
import com.wechat.pay.java.service.refund.model.AmountReq;
import com.wechat.pay.java.service.refund.model.CreateRequest;
/**
* VIEWController
@@ -42,6 +47,8 @@ import com.ruoyi.common.core.page.TableDataInfo;
@RequestMapping("/system/view")
public class OrderViewController extends BaseController
{
private static final Logger log = LoggerFactory.getLogger(OrderViewController.class);
@Autowired
private IOrderViewService orderViewService;
@Autowired
@@ -272,6 +279,24 @@ public class OrderViewController extends BaseController
Boolean approved = (Boolean) params.get("approved");
String reason = params.get("reason") != null ? params.get("reason").toString() : "";
if (approved != null && approved) {
// 调用微信退款接口
try {
CreateRequest request = new CreateRequest();
request.setOutRefundNo(order.getYuliu5());
request.setOutTradeNo(order.getYuliu5());
request.setTransactionId(order.getYuliu6());
request.setNotifyUrl(WXPayConstants.REFUND_NOTIFY_URL);
AmountReq amount = new AmountReq();
long total = (long) (100 * Double.parseDouble(order.getYuguMoney()));
amount.setTotal(total);
amount.setRefund(total);
amount.setCurrency("CNY");
request.setAmount(amount);
iRlzOrderService.refundOrder(request);
} catch (Exception e) {
log.error("微信退款调用失败", e);
return AjaxResult.error("微信退款调用失败: " + e.getMessage());
}
order.setStatus("7"); // 已退款
order.setRemark((order.getRemark() != null ? order.getRemark() : "") + " [退款审核通过:" + reason + "]");
} else {

View File

@@ -204,15 +204,28 @@ public AjaxResult completeOrder(@PathVariable("orderId") Long orderId)
try {
Long userId=this.getLoginUser().getUserId();
SysUser sysUser = iSysUserService.selectUserById(userId);
if(StringUtils.isEmpty(sysUser.getOpenid())){
return a.error("用户未绑定微信openid,无法发起支付");
}
String money=order.getYuguMoney();
int total= (int) (100*Double.parseDouble(order.getYuguMoney()));
a= AjaxResult.success(V3WXPayUtil.createOrder(String.valueOf(orderId), sysUser.getOpenid(), "订单支付",total));
} catch (Exception e) {
e.printStackTrace();
return a.error("支付请求失败: " + e.getMessage());
}
return a;
}
/**
* 客户申请退款 (status 4/8 -> 5)
*/
@PostMapping(value = "/applyRefund/{orderId}")
public AjaxResult applyRefund(@PathVariable Long orderId, @RequestBody(required = false) RlzOrder req) {
String reason = req != null ? req.getRemark() : null;
return AjaxResult.success(rlzOrderService.applyRefundByOrderId(orderId, reason));
}
@GetMapping(value = "/refundOrder/{orderId}")
public AjaxResult refundOrder(@PathVariable Long orderId) throws GeneralSecurityException, UnsupportedEncodingException {
AjaxResult a=new AjaxResult();

View File

@@ -0,0 +1,51 @@
package com.ruoyi.system.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.domain.RlzPlatformConfig;
import com.ruoyi.system.service.IRlzPlatformConfigService;
@RestController
@RequestMapping("/system/platformConfig")
public class RlzPlatformConfigController extends BaseController
{
@Autowired
private IRlzPlatformConfigService platformConfigService;
@GetMapping("/list")
public TableDataInfo list(RlzPlatformConfig config) {
startPage();
List<RlzPlatformConfig> list = platformConfigService.selectRlzPlatformConfigList(config);
return getDataTable(list);
}
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable Long id) {
return AjaxResult.success(platformConfigService.selectRlzPlatformConfigById(id));
}
@GetMapping("/getValue/{configKey}")
public AjaxResult getValue(@PathVariable String configKey) {
String value = platformConfigService.getConfigValue(configKey, "");
return AjaxResult.success("ok", value);
}
@PostMapping
public AjaxResult add(@RequestBody RlzPlatformConfig config) {
return toAjax(platformConfigService.insertRlzPlatformConfig(config));
}
@PutMapping
public AjaxResult edit(@RequestBody RlzPlatformConfig config) {
return toAjax(platformConfigService.updateRlzPlatformConfig(config));
}
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(platformConfigService.deleteRlzPlatformConfigByIds(ids));
}
}

View File

@@ -130,4 +130,15 @@ public class RlzServiceTypeController extends BaseController
List<RlzPriceHistory> list = rlzServiceTypeService.selectPriceHistoryByServiceTypeId(serviceTypeId);
return AjaxResult.success(list);
}
/**
* 小程序端获取启用的服务列表(无需登录)
*/
@GetMapping("/enabled")
public AjaxResult enabled(RlzServiceType rlzServiceType)
{
rlzServiceType.setStatus("0");
List<RlzServiceType> list = rlzServiceTypeService.selectRlzServiceTypeList(rlzServiceType);
return AjaxResult.success(list);
}
}

View File

@@ -0,0 +1,44 @@
package com.ruoyi.system.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.domain.RlzOrder;
import com.ruoyi.system.mapper.RlzOrderMapper;
import com.ruoyi.system.service.IRlzSettlementService;
@RestController
@RequestMapping("/system/settlement")
public class RlzSettlementController extends BaseController
{
@Autowired
private IRlzSettlementService settlementService;
@Autowired
private RlzOrderMapper orderMapper;
// 待结算订单列表 (status=4 已支付 AND is_zhifu != '1' 未打款)
@GetMapping("/pending")
public TableDataInfo pending(RlzOrder order) {
startPage();
order.setStatus("4");
order.setIsZhifu("0");
List<RlzOrder> list = orderMapper.selectRlzOrderList(order);
return getDataTable(list);
}
@PostMapping("/settle/{orderId}")
public AjaxResult settle(@PathVariable Long orderId) {
settlementService.settleOrder(orderId);
return AjaxResult.success("结算完成");
}
@PostMapping("/batchSettle")
public AjaxResult batchSettle(@RequestBody Long[] orderIds) {
settlementService.batchSettleOrder(orderIds);
return AjaxResult.success("批量结算完成");
}
}

View File

@@ -43,7 +43,7 @@ public class SysOssController extends BaseController
private ISysOssService sysOssService;
@Autowired
private ISysOssConfigService sysOssConfigService;
final String endpoint="http://81.68.125.247:9000";
final String endpoint="http://101.43.95.130:9000";
final String bucketName="rlz";
/** 自定义上传Minio服务器方法*/

View File

@@ -0,0 +1,34 @@
package com.ruoyi.system.domain;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
public class RlzPlatformConfig extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long id;
@Excel(name = "配置键")
private String configKey;
@Excel(name = "配置值")
private String configValue;
@Excel(name = "描述")
private String description;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getConfigKey() { return configKey; }
public void setConfigKey(String configKey) { this.configKey = configKey; }
public String getConfigValue() { return configValue; }
public void setConfigValue(String configValue) { this.configValue = configValue; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
@Override
public String toString() {
return "RlzPlatformConfig{id=" + id + ", configKey='" + configKey + "', configValue='" + configValue + "'}";
}
}

View File

@@ -43,6 +43,9 @@ public class RlzServiceType extends BaseEntity
@Excel(name = "状态", readConverterExp = "0=启用,1=停用")
private String status;
/** 图标URL */
private String icon;
/** 排序 */
private Integer sortOrder;
@@ -116,6 +119,16 @@ public class RlzServiceType extends BaseEntity
return status;
}
public void setIcon(String icon)
{
this.icon = icon;
}
public String getIcon()
{
return icon;
}
public void setSortOrder(Integer sortOrder)
{
this.sortOrder = sortOrder;
@@ -136,6 +149,7 @@ public class RlzServiceType extends BaseEntity
.append("price", getPrice())
.append("description", getDescription())
.append("status", getStatus())
.append("icon", getIcon())
.append("sortOrder", getSortOrder())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())

View File

@@ -93,4 +93,9 @@ public interface RlzOrderMapper
* 仪表盘-陪护服务排名
*/
public List<java.util.Map<String, Object>> getCaregiverRanking();
/**
* 陪护员本月统计(收入+接单数)
*/
public java.util.Map<String, Object> getCaregiverMonthlyStats(Long bId);
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.RlzPlatformConfig;
public interface RlzPlatformConfigMapper
{
List<RlzPlatformConfig> selectRlzPlatformConfigList(RlzPlatformConfig config);
RlzPlatformConfig selectRlzPlatformConfigById(Long id);
RlzPlatformConfig selectRlzPlatformConfigByKey(String configKey);
int insertRlzPlatformConfig(RlzPlatformConfig config);
int updateRlzPlatformConfig(RlzPlatformConfig config);
int deleteRlzPlatformConfigById(Long id);
int deleteRlzPlatformConfigByIds(Long[] ids);
}

View File

@@ -77,6 +77,11 @@ public interface IRlzOrderService
Long insertOrderPz(RlzOrder rlzOrder);
/**
* 客户申请退款 (status 4/8 -> 5)
*/
String applyRefundByOrderId(Long orderId, String reason);
List<java.util.Map<String, Object>> getIncomeStats(java.util.Map<String, Object> params);
/**
@@ -108,4 +113,9 @@ public interface IRlzOrderService
* 仪表盘-陪护服务排名
*/
List<java.util.Map<String, Object>> getCaregiverRanking();
/**
* 陪护员本月统计(收入+接单数)
*/
java.util.Map<String, Object> getCaregiverMonthlyStats(Long bId);
}

View File

@@ -0,0 +1,14 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.RlzPlatformConfig;
public interface IRlzPlatformConfigService
{
List<RlzPlatformConfig> selectRlzPlatformConfigList(RlzPlatformConfig config);
RlzPlatformConfig selectRlzPlatformConfigById(Long id);
String getConfigValue(String configKey, String defaultValue);
int insertRlzPlatformConfig(RlzPlatformConfig config);
int updateRlzPlatformConfig(RlzPlatformConfig config);
int deleteRlzPlatformConfigByIds(Long[] ids);
}

View File

@@ -0,0 +1,10 @@
package com.ruoyi.system.service;
import com.ruoyi.system.domain.RlzOrder;
public interface IRlzSettlementService
{
void calculateOrderFee(RlzOrder order);
void settleOrder(Long orderId);
void batchSettleOrder(Long[] orderIds);
}

View File

@@ -48,6 +48,7 @@ import org.springframework.stereotype.Service;
import com.ruoyi.system.mapper.RlzOrderMapper;
import com.ruoyi.system.domain.RlzOrder;
import com.ruoyi.system.service.IRlzOrderService;
import com.ruoyi.system.service.IRlzSettlementService;
import org.springframework.transaction.annotation.Transactional;
import static com.wechat.pay.java.core.util.GsonUtil.toJson;
@@ -66,6 +67,8 @@ public class RlzOrderServiceImpl implements IRlzOrderService
private RlzOrderMapper rlzOrderMapper;
@Autowired
private ISysWeixinPayService iSysWeixinPayService;
@Autowired
private IRlzSettlementService settlementService;
/**
* 查询【请填写功能名称】
*
@@ -226,6 +229,8 @@ public String weixinPayOrderNext(String decryptOrder) {
rlzOrder.setStatus("4");//已完成(已支付)
int i = rlzOrderMapper.updateRlzOrder(rlzOrder);
if(i>0){
// 自动计算分账金额
settlementService.calculateOrderFee(rlzOrder);
return "SUCCESS";
}else {
return "FAIL" +
@@ -379,4 +384,37 @@ public String weixinPayOrderNext(String decryptOrder) {
public List<java.util.Map<String, Object>> getCaregiverRanking() {
return rlzOrderMapper.getCaregiverRanking();
}
@Override
public String applyRefundByOrderId(Long orderId, String reason) {
RlzOrder order = this.selectRlzOrderByOrderId(orderId);
if (order == null) {
return "订单不存在!";
}
String status = order.getStatus();
if (!"4".equals(status) && !"8".equals(status)) {
return "当前订单状态不允许申请退款!";
}
order.setStatus("5");
if (reason != null && !reason.isEmpty()) {
order.setRemark((order.getRemark() != null ? order.getRemark() : "") + " [退款原因:" + reason + "]");
}
int i = this.updateRlzOrder(order);
if (i > 0) {
return "退款申请提交成功!";
} else {
return "退款申请提交失败!";
}
}
@Override
public java.util.Map<String, Object> getCaregiverMonthlyStats(Long bId) {
java.util.Map<String, Object> result = rlzOrderMapper.getCaregiverMonthlyStats(bId);
if (result == null) {
result = new HashMap<>();
result.put("income", 0);
result.put("orderCount", 0);
}
return result;
}
}

View File

@@ -0,0 +1,46 @@
package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.RlzPlatformConfig;
import com.ruoyi.system.mapper.RlzPlatformConfigMapper;
import com.ruoyi.system.service.IRlzPlatformConfigService;
@Service
public class RlzPlatformConfigServiceImpl implements IRlzPlatformConfigService
{
@Autowired
private RlzPlatformConfigMapper platformConfigMapper;
@Override
public List<RlzPlatformConfig> selectRlzPlatformConfigList(RlzPlatformConfig config) {
return platformConfigMapper.selectRlzPlatformConfigList(config);
}
@Override
public RlzPlatformConfig selectRlzPlatformConfigById(Long id) {
return platformConfigMapper.selectRlzPlatformConfigById(id);
}
@Override
public String getConfigValue(String configKey, String defaultValue) {
RlzPlatformConfig config = platformConfigMapper.selectRlzPlatformConfigByKey(configKey);
return (config != null && config.getConfigValue() != null) ? config.getConfigValue() : defaultValue;
}
@Override
public int insertRlzPlatformConfig(RlzPlatformConfig config) {
return platformConfigMapper.insertRlzPlatformConfig(config);
}
@Override
public int updateRlzPlatformConfig(RlzPlatformConfig config) {
return platformConfigMapper.updateRlzPlatformConfig(config);
}
@Override
public int deleteRlzPlatformConfigByIds(Long[] ids) {
return platformConfigMapper.deleteRlzPlatformConfigByIds(ids);
}
}

View File

@@ -0,0 +1,133 @@
package com.ruoyi.system.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.domain.JiaoyiDetail;
import com.ruoyi.system.domain.RlzOrder;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.mapper.JiaoyiDetailMapper;
import com.ruoyi.system.mapper.RlzOrderMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.IRlzPlatformConfigService;
import com.ruoyi.system.service.IRlzSettlementService;
@Service
public class RlzSettlementServiceImpl implements IRlzSettlementService
{
private static final Logger log = LoggerFactory.getLogger(RlzSettlementServiceImpl.class);
@Autowired
private IRlzPlatformConfigService platformConfigService;
@Autowired
private RlzOrderMapper orderMapper;
@Autowired
private SysUserMapper userMapper;
@Autowired
private JiaoyiDetailMapper jiaoyiDetailMapper;
@Override
public void calculateOrderFee(RlzOrder order) {
String yuguMoney = order.getYuguMoney();
if (yuguMoney == null || yuguMoney.isEmpty()) {
log.warn("Order {} has no yuguMoney, skip calculation", order.getOrderId());
return;
}
// 获取抽成比例,默认20%
String rateStr = platformConfigService.getConfigValue("commission_rate", "20");
BigDecimal rate = new BigDecimal(rateStr);
BigDecimal total = new BigDecimal(yuguMoney);
BigDecimal kouchu = total.multiply(rate).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
BigDecimal fanxian = total.subtract(kouchu);
order.setJiesuanMoney(yuguMoney);
order.setKouchuMoney(kouchu.toString());
order.setFanxianMoney(fanxian.toString());
order.setFanxianTax(rateStr);
order.setIsZhifu("0");
RlzOrder update = new RlzOrder();
update.setOrderId(order.getOrderId());
update.setJiesuanMoney(yuguMoney);
update.setKouchuMoney(kouchu.toString());
update.setFanxianMoney(fanxian.toString());
update.setFanxianTax(rateStr);
update.setIsZhifu("0");
orderMapper.updateRlzOrder(update);
log.info("Order {} fee calculated: total={}, kouchu={}, fanxian={}", order.getOrderId(), total, kouchu, fanxian);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void settleOrder(Long orderId) {
RlzOrder order = orderMapper.selectRlzOrderByOrderId(orderId);
if (order == null) {
throw new ServiceException("订单不存在");
}
if (!"4".equals(order.getStatus())) {
throw new ServiceException("订单未支付,无法结算");
}
if ("1".equals(order.getIsZhifu())) {
throw new ServiceException("已打款,请勿重复操作");
}
// 如果还没计算分账,先计算
if (order.getFanxianMoney() == null || order.getFanxianMoney().isEmpty()) {
calculateOrderFee(order);
order = orderMapper.selectRlzOrderByOrderId(orderId); // 重新获取
}
Long caregiverId = order.getbId();
BigDecimal fanxianMoney = new BigDecimal(order.getFanxianMoney());
// 更新订单状态
RlzOrder update = new RlzOrder();
update.setOrderId(orderId);
update.setIsZhifu("1");
update.setStatus("8");
orderMapper.updateRlzOrder(update);
// 更新陪诊员余额
SysUser caregiver = userMapper.selectUserById(caregiverId);
if (caregiver != null) {
BigDecimal oldBalance = new BigDecimal(caregiver.getYue() != null && !caregiver.getYue().isEmpty() ? caregiver.getYue() : "0");
BigDecimal newBalance = oldBalance.add(fanxianMoney);
caregiver.setYue(newBalance.toString());
BigDecimal oldTotal = new BigDecimal(caregiver.getLeijijine() != null && !caregiver.getLeijijine().isEmpty() ? caregiver.getLeijijine() : "0");
caregiver.setLeijijine(oldTotal.add(fanxianMoney).toString());
userMapper.updateUser(caregiver);
// 生成交易记录
JiaoyiDetail detail = new JiaoyiDetail();
detail.setUserId(caregiverId);
detail.setType("1");
detail.setJine(fanxianMoney.toString());
detail.setYue(newBalance.toString());
detail.setName("订单结算 - " + (order.getYuliu1() != null ? order.getYuliu1() : orderId));
detail.setCreateTime(new Date());
jiaoyiDetailMapper.insertJiaoyiDetail(detail);
log.info("Order {} settled: caregiver={}, fanxian={}, newBalance={}", orderId, caregiverId, fanxianMoney, newBalance);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchSettleOrder(Long[] orderIds) {
for (Long id : orderIds) {
settleOrder(id);
}
}
}

View File

@@ -23,7 +23,7 @@ import java.util.UUID;
*/
@Component
public class MinioUtil {
final String endpoint="http://81.68.125.247:9000";
final String endpoint="http://rlz-minio:9000";
final String accessKey="minioadmin";
final String secretKey="minioadmin";
final String bucketName="rlz";

View File

@@ -33,32 +33,23 @@ import java.util.Map;
public class V3WXPayUtil {
public static Map<String, Object> createOrder(String buildCode, String openId, String description, int total) throws Exception {
// PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
// new ByteArrayInputStream(WXPayConstants.PRIVATE_KEY.getBytes("utf-8")));
public static Map<String, Object> createOrder(String orderIdStr, String openId, String description, int total) throws Exception {
try {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(WXPayConstants.PRIVATE_KEY);
// writeText("C:/log.txt", "获取私钥:=========>" + merchantPrivateKey.toString());
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// writeText("C:/log.txt", "获取证书管理器实例:=========>" + certificatesManager.toString() + "\n");
// 向证书管理器增加需要自动更新平台证书的商户信息
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(WXPayConstants.MCH_ID,
new PrivateKeySigner(WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey));
// writeText("C:/log.txt", "获取管理器增加需要自动更新平台证书的商户信息前:=========>" + wechatPay2Credentials.toString() + "\n");
byte[] api_v3KEYBytes = WXPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8);
// writeText("C:/log.txt", "获取api_v3KEYBytes:=========>" + api_v3KEYBytes + "\n");
certificatesManager.putMerchant(WXPayConstants.MCH_ID, wechatPay2Credentials, api_v3KEYBytes);
// writeText("C:/log.txt", "证书管理器实例:=========>" + certificatesManager.toString());
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(WXPayConstants.MCH_ID);
// writeText("C:/log.txt", "从证书管理器中获取verifier:=========>" + verifier);
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(WXPayConstants.MCH_ID, WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
.build();
// writeText("C:/log.txt", "从证书管理器中获取verifier后:=========>" + "\n");
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
@@ -66,14 +57,17 @@ public class V3WXPayUtil {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
// out_trade_no 要求6-32字符,用orderId左补零到6位
String outTradeNo = String.format("%06d", Long.parseLong(orderIdStr));
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid", WXPayConstants.MCH_ID)
.put("appid", WXPayConstants.APP_ID)
.put("notify_url", WXPayConstants.NOTIFY_URL)
.put("description", description)
.put("out_trade_no", buildCode);
.put("out_trade_no", outTradeNo);
rootNode.putObject("amount")
.put("total", total) // 支付总金额,分为单位 需前端传入
.put("total", total)
.put("currency", "CNY");
rootNode.putObject("payer")
.put("openid", openId);
@@ -84,41 +78,43 @@ public class V3WXPayUtil {
CloseableHttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
// log.info("成功获取微信预支付订单号{}", bodyAsString);
System.out.println("成功获取微信预支付订单号==========>" + bodyAsString);
System.out.println("微信预支付应答==========>" + bodyAsString);
JsonNode node = objectMapper.readTree(bodyAsString);
// 检查微信返回是否为错误响应
JsonNode prepayIdNode = node.get("prepay_id");
if (prepayIdNode == null) {
String errorMsg = "微信支付下单失败: " + bodyAsString;
System.out.println(errorMsg);
throw new RuntimeException(errorMsg);
}
// 时间戳
String currentTimeMillis = System.currentTimeMillis() / 1000 + "";
// 随机字符串 hutool工具类
// 随机字符串
String randomString = RandomUtil.randomString(32);
StringBuilder builder = new StringBuilder();
// 应用ID
builder.append(WXPayConstants.APP_ID).append("\n");
// 时间戳
builder.append(currentTimeMillis).append("\n");
// 随机字符串
builder.append(randomString).append("\n");
JsonNode node = objectMapper.readTree(bodyAsString);
// writeText("C:/log.txt", "从获取node后:=========>" + node + "\n");
// 订单详情扩展字符串
builder.append("prepay_id=").append(node.get("prepay_id").toString().replace("\"", "")).append("\n");
builder.append("prepay_id=").append(prepayIdNode.toString().replace("\"", "")).append("\n");
String paySign = sign(builder.toString().getBytes());
Map<String, Object> map = new HashMap();
map.put("package", "prepay_id=" + node.get("prepay_id").toString().replace("\"", ""));
map.put("prepayId", node.get("prepay_id").toString().replace("\"", ""));
map.put("package", "prepay_id=" + prepayIdNode.toString().replace("\"", ""));
map.put("prepayId", prepayIdNode.toString().replace("\"", ""));
map.put("nonceStr", randomString);
map.put("signType", "RSA");
map.put("paySign", paySign);
map.put("timeStamp", currentTimeMillis);
map.put("appId", WXPayConstants.APP_ID);
map.put("orderId", buildCode);
map.put("orderId", orderIdStr);
return map;
}catch (Exception e){
// writeText("C:/log.txt", "异常:" + getStackTraceMessage(e));
throw e;
}
}

View File

@@ -15,10 +15,10 @@ public class WXPayConstants {
public static final String MCH_SERIAL_NO = "343F1DCA192D001C9689BFDA2B595E3B7D917B45";
/*APIv3密钥*/
public static final String API_V3KEY = "RENJIANBOchengjiepurenfeitangjie";
/*支付成功回调地址*/
public static final String NOTIFY_URL = "https://ruilaizipj.com/system/weixin/wxPayNotify";
/*支付成功回调地址(通过/prod-api/前缀让nginx路由到RLZ后端)*/
public static final String NOTIFY_URL = "https://ruilaizipj.com/prod-api/system/weixin/wxPayNotify";
/*退款回调地址*/
public static final String REFUND_NOTIFY_URL = "https://ruilaizipj.com/system/weixin/wxRefundNotify";
public static final String REFUND_NOTIFY_URL = "https://ruilaizipj.com/prod-api/system/weixin/wxRefundNotify";
// /*支付成功回调地址*/
// public static final String NOTIFY_URL = "https://35q45673j2.goho.co/system/weixin/wxPayNotify";
// /*退款回调地址*/

View File

@@ -294,4 +294,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
GROUP BY date
ORDER BY date
</select>
<select id="getCaregiverMonthlyStats" parameterType="long" resultType="map">
SELECT
COUNT(*) as orderCount,
COALESCE(SUM(CASE WHEN status IN ('4','8') THEN CAST(COALESCE(NULLIF(fanxian_money,''), '0') AS DECIMAL(10,2)) ELSE 0 END), 0) as income
FROM rlz_order
WHERE b_id = #{bId}
AND DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')
</select>
</mapper>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.RlzPlatformConfigMapper">
<resultMap type="RlzPlatformConfig" id="RlzPlatformConfigResult">
<result property="id" column="id"/>
<result property="configKey" column="config_key"/>
<result property="configValue" column="config_value"/>
<result property="description" column="description"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectVo">
select id, config_key, config_value, description, create_time, update_time from rlz_platform_config
</sql>
<select id="selectRlzPlatformConfigList" parameterType="RlzPlatformConfig" resultMap="RlzPlatformConfigResult">
<include refid="selectVo"/>
<where>
<if test="configKey != null and configKey != ''"> and config_key like concat('%', #{configKey}, '%')</if>
<if test="description != null and description != ''"> and description like concat('%', #{description}, '%')</if>
</where>
</select>
<select id="selectRlzPlatformConfigById" parameterType="Long" resultMap="RlzPlatformConfigResult">
<include refid="selectVo"/> where id = #{id}
</select>
<select id="selectRlzPlatformConfigByKey" parameterType="String" resultMap="RlzPlatformConfigResult">
<include refid="selectVo"/> where config_key = #{configKey}
</select>
<insert id="insertRlzPlatformConfig" parameterType="RlzPlatformConfig" useGeneratedKeys="true" keyProperty="id">
insert into rlz_platform_config
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="configKey != null">config_key,</if>
<if test="configValue != null">config_value,</if>
<if test="description != null">description,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="configKey != null">#{configKey},</if>
<if test="configValue != null">#{configValue},</if>
<if test="description != null">#{description},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
</trim>
</insert>
<update id="updateRlzPlatformConfig" parameterType="RlzPlatformConfig">
update rlz_platform_config
<trim prefix="SET" suffixOverrides=",">
<if test="configKey != null">config_key = #{configKey},</if>
<if test="configValue != null">config_value = #{configValue},</if>
<if test="description != null">description = #{description},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteRlzPlatformConfigById" parameterType="Long">
delete from rlz_platform_config where id = #{id}
</delete>
<delete id="deleteRlzPlatformConfigByIds" parameterType="String">
delete from rlz_platform_config where id in
<foreach item="id" collection="array" open="(" separator="," close=")">#{id}</foreach>
</delete>
</mapper>

View File

@@ -12,6 +12,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="price" column="price" />
<result property="description" column="description" />
<result property="status" column="status" />
<result property="icon" column="icon" />
<result property="sortOrder" column="sort_order" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
@@ -21,7 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectRlzServiceTypeVo">
select id, name, category, type_code, price, description, status, sort_order,
select id, name, category, type_code, price, description, status, icon, sort_order,
create_by, create_time, update_by, update_time, remark
from rlz_service_type
</sql>
@@ -51,6 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="price != null">price,</if>
<if test="description != null">description,</if>
<if test="status != null">status,</if>
<if test="icon != null">icon,</if>
<if test="sortOrder != null">sort_order,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
@@ -63,6 +65,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="price != null">#{price},</if>
<if test="description != null">#{description},</if>
<if test="status != null">#{status},</if>
<if test="icon != null">#{icon},</if>
<if test="sortOrder != null">#{sortOrder},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
@@ -79,6 +82,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="price != null">price = #{price},</if>
<if test="description != null">description = #{description},</if>
<if test="status != null">status = #{status},</if>
<if test="icon != null">icon = #{icon},</if>
<if test="sortOrder != null">sort_order = #{sortOrder},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>