;
}
declare interface _Upload {
diff --git a/uni_modules/wmf-code/README.md b/uni_modules/wmf-code/README.md
new file mode 100644
index 0000000..231b420
--- /dev/null
+++ b/uni_modules/wmf-code/README.md
@@ -0,0 +1,84 @@
+
+ 文档地址
+
+https://meet-ui.com/#/
+为了能够第一时间解决问题,为了不影响各位在项目中使用特此提供微信群交流
+
+
+
+
+
+# 温馨提示
+
+如果使用cli创建的应用请在`vue.config.js`里面添加如下配置
+```
+module.exports = {
+ transpileDependencies: [
+ /[/\\]node_modules[/\\](.+?)?@uni-ui(.*)[/\\]code-plugs/,
+ /[/\\]node_modules[/\\](.+?)?@uni-ui(.*)[/\\]code-ui/
+ ]
+}
+```
+# 微信小程序特别说明
+
+如果在弹窗里面使用,并且弹窗有动画 当弹窗显示的时候二维码会发生偏移或抖动
+因为微信小程序的canvas在真机预览渲染的是原生组件并不是 h5的canavas 所以在开发工具里面没问题但是真机会有问题
+
+解决方案:
+
+ 1. 如果是使用uView组件的弹窗 请关闭动画
+ 2. 如果是使用uni-popup组件 关闭动画无效 因为uni的弹窗动画关闭 其实是动画时长为0并且是包含在uni-transition详情请查看uni-popup代码
+
+## 如果是使用uni_modules而非Npm
+
+在`pages.json`里面配置如下
+
+```js
+"easycom": {
+ "^w-(.*)": "@/uni_modules/wmf-code/components/w-$1/w-$1.vue"//二维码条形码的配置 如果是uni_modules
+},
+"pages": [
+ //...
+]
+```
+
+## 如果是使用npm而非uni_modules
+
+在`pages.json`里面配置如下
+
+```js
+"easycom": {
+ "^w-(.*)": "@uni-ui/code-ui/components/w-$1/index.vue"//二维码条形码的配置 如果是npm方式使用
+},
+"pages": [
+ //...
+]
+```
+
+## 条形码支持添加文字
+
+
+
+
+
+
+
+
+
+
+
+#
+
+
+
+
+
+
+
+#
+
+
+
+
+
+
diff --git a/uni_modules/wmf-code/changelog.md b/uni_modules/wmf-code/changelog.md
new file mode 100644
index 0000000..7d21184
--- /dev/null
+++ b/uni_modules/wmf-code/changelog.md
@@ -0,0 +1,108 @@
+## 1.6.2(2022-10-24)
+* 修复已知问题
+## 1.6.1(2022-10-19)
+* 支付宝小程序适配问题
+* 修改文档
+## 1.6.0(2022-07-27)
+* 修改条形码bug
+## 1.5.9(2022-07-26)
+* 修复条形码保存图片部分黑底问题
+## 1.5.8(2022-07-26)
+* 修复支付宝小程序图片保存缺失问题
+## 1.5.7(2022-07-09)
+* 修复条形码保存缺失问题
+## 1.5.6(2022-07-01)
+修改引入错误
+## 1.5.5(2022-07-01)
+* 修复vue3无法通过ref获取保存图片方法
+## 1.5.4(2022-06-01)
+* 条形码支持添加文字
+## 1.5.3(2022-05-31)
+* 修复二维码有时候无法识别问题
+* 建议使用npm方式
+## 1.5.2(2022-05-31)
+修改条形码垂直方向生成图片 参数互换
+## 1.5.1(2022-05-31)
+条形码增加垂直方向 默认水平方向
+## 1.5.0(2022-05-30)
+修改uni_modules引入错误
+## 1.4.9(2022-05-25)
+支持vue3
+## 1.4.8(2021-12-06)
+* 百度小程序不支持canvas的arcTo方法 如果在百度小程序中不支持的全部默认值
+* 小程序平台绘制文字不支持渐变 取color数组第一个
+## 1.4.7(2021-12-05)
+* 修复已知问题
+## 1.4.5(2021-12-05)
+* 修复已知问题
+## 1.4.4(2021-11-30)
+* level默认值修改为2
+## 1.4.3(2021-11-30)
+* 修复引用js_sdk引用路径
+## 1.4.2(2021-11-29)
+* 修复设置画布大小时没有带单位px导致显示不全
+## 1.4.1(2021-11-29)
+1. 修复在高分辨率屏幕上支付宝小程序显示模糊问题
+2. 修复保存相册图片模糊问题
+## 1.4.0(2021-11-23)
+1. 使用TS重构
+2. 新增二维码文字绘制
+3. 新增二维码padding
+4. 新增二维码中间图片圆角 圆形
+5. 新增长按事件
+6. 二维码边框支持透明
+7. 颜色最多支持10中颜色渐变
+## 1.3.7(2021-11-08)
+修改引入
+## 1.3.6(2021-11-02)
+修改引用路径
+## 1.3.5(2021-10-19)
+canvas 增加 type="2d"
+## 1.3.4(2021-10-14)
+1. 修改百度小程序修改参数无法重新渲染问题
+2. 修改纯中文无法识别问题
+## 1.3.3(2021-09-04)
+修改uni_module引用路径
+## 1.3.2(2021-09-03)
+* 【重要】多个条形码或者二维码同时渲染
+* 增加示例
+## 1.3.1(2021-08-31)
+uni_modules支持组件方式
+## 1.3.0(2021-08-31)
+支持纯文字以及文字数字 字母的混合
+## 1.2.9(2021-08-30)
+新增二维码创建成功回调方法中返回生成的二维码图片
+## 1.2.8(2021-08-20)
+支持vue3
+## 1.2.7(2021-08-20)
+修改版本号
+## 1.2.6(2021-06-28)
+1. 修改自定义组件时微信小程序无法显示二维码的问题
+2. 统一二维码的大小单位为rpx
+## 1.2.5(2021-06-25)
+二维码logo大小增加默认值:30
+## 1.2.4(2021-06-24)
+1. nvue 二维码支持渐变色
+## 1.2.3(2021-06-23)
+1. 修改app不支持颜色渐变问题
+2. 修改判断方式Object.prototype.toString.call()
+## 1.2.2(2021-06-23)
+修改插件描述
+## 1.2.1(2021-06-23)
+修改return 错误
+## 1.1.2(2021-06-23)
+修改文档
+## 1.1.1(2021-06-23)
+ 二维码支持渐变色
+## 1.0.8(2021-06-22)
+兼容nvue
+## 1.0.7(2021-06-09)
+1. 修改保存二维码图片的方法名以及传参方式
+2. 修改微信小程序保存二维码时显示为黑色
+3. 二维码增加参数
+## 1.0.6(2021-06-04)
+修改 description说明
+## 1.0.5(2021-06-04)
+修改文档展示名称
+## 1.0.4(2021-06-04)
+修改符合uni_modules规范
diff --git a/uni_modules/wmf-code/common/helper.js b/uni_modules/wmf-code/common/helper.js
new file mode 100644
index 0000000..b150cd7
--- /dev/null
+++ b/uni_modules/wmf-code/common/helper.js
@@ -0,0 +1,105 @@
+// 判断arr是否为一个数组,返回一个bool值
+function isArray(arr) {
+ return Object.prototype.toString.call(arr) === '[object Array]';
+}
+// 深度克隆
+function deepClone(obj) {
+ // 对常见的“非”值,直接返回原来值
+ if ([null, undefined, NaN, false].includes(obj)) return obj;
+ if (typeof obj !== "object" && typeof obj !== 'function') {
+ //原始类型直接返回
+ return obj;
+ }
+ var o = isArray(obj) ? [] : {};
+ for (let i in obj) {
+ if (obj.hasOwnProperty(i)) {
+ o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
+ }
+ }
+ return o;
+}
+
+function getUUid(len = 32, firstU = true, radix = null) {
+ let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
+ let uuid = [];
+ radix = radix || chars.length;
+
+ if (len) {
+ // 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
+ for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
+ } else {
+ let r;
+ // rfc4122标准要求返回的uuid中,某些位为固定的字符
+ uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
+ uuid[14] = '4';
+
+ for (let i = 0; i < 36; i++) {
+ if (!uuid[i]) {
+ r = 0 | Math.random() * 16;
+ uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
+ }
+ }
+ }
+ // 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
+ if (firstU) {
+ uuid.shift();
+ return 'u' + uuid.join('');
+ } else {
+ return uuid.join('');
+ }
+}
+function platform () {
+ let val = null;
+ // #ifdef VUE3
+ val = 'VUE3'
+ // #endif
+ // #ifdef APP-PLUS
+ val = 'APP-PLUS'
+ // #endif
+ // #ifdef APP-PLUS-NVUE || APP-NVUE
+ val = 'NVUE'
+ // #endif
+ // #ifdef H5
+ val = 'H5'
+ // #endif
+ // #ifdef MP-WEIXIN
+ val = 'MP-WEIXIN'
+ // #endif
+ // #ifdef MP-ALIPAY
+ val = 'MP-ALIPAY'
+ // #endif
+ // #ifdef MP-BAIDU
+ val = 'MP-BAIDU'
+ // #endif
+ // #ifdef MP-TOUTIAO
+ val = 'MP-TOUTIAO'
+ // #endif
+ // #ifdef MP-LARK
+ val = 'MP-LARK'
+ // #endif
+ // #ifdef MP-TOUTIAO
+ val = 'MP-QQ'
+ // #endif
+ // #ifdef MP-KUAISHOU
+ val = 'MP-KUAISHOU'
+ // #endif
+ // #ifdef MP-360
+ val = 'MP-360'
+ // #endif
+ // #ifdef QUICKAPP-WEBVIEW
+ val = 'QUICKAPP-WEBVIEW'
+ // #endif
+ // #ifdef QUICKAPP-WEBVIEW-UNION
+ val = 'QUICKAPP-WEBVIEW-UNION'
+ // #endif
+ // #ifdef QUICKAPP-WEBVIEW-HUAWEI
+ val = 'QUICKAPP-WEBVIEW-HUAWEI'
+ // #endif
+ return val;
+
+}
+export {
+ deepClone,
+ getUUid,
+ platform
+};
diff --git a/uni_modules/wmf-code/components/w-barcode/w-barcode.vue b/uni_modules/wmf-code/components/w-barcode/w-barcode.vue
new file mode 100644
index 0000000..3661053
--- /dev/null
+++ b/uni_modules/wmf-code/components/w-barcode/w-barcode.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uni_modules/wmf-code/components/w-qrcode/w-qrcode.vue b/uni_modules/wmf-code/components/w-qrcode/w-qrcode.vue
new file mode 100644
index 0000000..e2f2f36
--- /dev/null
+++ b/uni_modules/wmf-code/components/w-qrcode/w-qrcode.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/uni_modules/wmf-code/js_sdk/index.js b/uni_modules/wmf-code/js_sdk/index.js
new file mode 100644
index 0000000..026287b
--- /dev/null
+++ b/uni_modules/wmf-code/js_sdk/index.js
@@ -0,0 +1 @@
+var __assign=function(){return(__assign=Object.assign||function(t){for(var e,i=1,r=arguments.length;in[0]&&e[1]>12&15),r+=String.fromCharCode(128|e>>6&63)):r+=String.fromCharCode(192|e>>6&31),r+=String.fromCharCode(128|e>>0&63));return r},SaveCodeImg=function(t){var i=UNIT_CONVERSION(Number(t.width)),r=UNIT_CONVERSION(Number(t.height)),e=getPixelRatio("pixelRatio"),o=i*e,s=r*e;return"MP-ALIPAY"==t.source&&(i=o,r=s),new Promise(function(e){"[object String]"==Object.prototype.toString.call(t.id)?uni.canvasToTempFilePath({canvasId:t.id,width:i,height:r,destWidth:o,destHeight:s,fileType:t.type||"jpg",quality:t.quality||1,complete:function(t){e(t)}},t.ctx):"[object Object]"==Object.prototype.toString.call(t.id)&&t.id.toTempFilePath(0,0,i,r,o,s,t.type||"png",1,function(t){e(t)})})},SetGradient=function(t,e,i,r){i=t.createLinearGradient(0,0,e,i);return 1===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(1,r[0])),2===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(1,r[1])),3===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(.5,r[1]),i.addColorStop(1,r[2])),4===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(.35,r[1]),i.addColorStop(.7,r[2]),i.addColorStop(1,r[3])),5===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(.35,r[1]),i.addColorStop(.6,r[2]),i.addColorStop(.8,r[3]),i.addColorStop(1,r[4])),6===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(.25,r[1]),i.addColorStop(.45,r[2]),i.addColorStop(.65,r[3]),i.addColorStop(.85,r[4]),i.addColorStop(1,r[5])),7===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(.15,r[1]),i.addColorStop(.35,r[2]),i.addColorStop(.45,r[3]),i.addColorStop(.65,r[4]),i.addColorStop(.85,r[5]),i.addColorStop(1,r[6])),8===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(.1,r[1]),i.addColorStop(.25,r[2]),i.addColorStop(.45,r[3]),i.addColorStop(.65,r[4]),i.addColorStop(.85,r[5]),i.addColorStop(.9,r[6]),i.addColorStop(1,r[7])),9===r.length&&(i.addColorStop(0,r[0]),i.addColorStop(.2,r[1]),i.addColorStop(.3,r[2]),i.addColorStop(.5,r[3]),i.addColorStop(.6,r[4]),i.addColorStop(.7,r[5]),i.addColorStop(.8,r[6]),i.addColorStop(.9,r[7]),i.addColorStop(1,r[8])),10<=r.length&&(i.addColorStop(0,r[0]),i.addColorStop(.1,r[1]),i.addColorStop(.2,r[2]),i.addColorStop(.3,r[3]),i.addColorStop(.4,r[4]),i.addColorStop(.5,r[5]),i.addColorStop(.6,r[6]),i.addColorStop(.7,r[7]),i.addColorStop(.85,r[8]),i.addColorStop(1,r[9])),i},QRCodeInit=function(){function t(t){void 0===t&&(t=2),this.strinbuf=[],this.eccbuf=[],this.qrframe=[],this.framask=[],this.rlens=[],this.genpoly=[],this.ecclevel=2,this.N1=3,this.N2=3,this.N3=40,this.N4=10,this.neccblk2=0,this.width=0,this.neccblk1=0,this.datablkw=0,this.eccblkwid=0,this.ecclevel=t}return t.prototype.setmask=function(t,e){var i=null;e>=1,this.framask[i+=t]=1},t.prototype.getWidth=function(){return this.width},t.prototype.putalign=function(t,e){this.qrframe[t+this.width*e]=1;for(var i=-2;i<2;i++)this.qrframe[t+i+this.width*(e-2)]=1,this.qrframe[t-2+this.width*(e+i+1)]=1,this.qrframe[t+2+this.width*(e+i)]=1,this.qrframe[t+i+1+this.width*(e+2)]=1;for(i=0;i<2;i++)this.setmask(t-1,e+i),this.setmask(t+1,e-i),this.setmask(t-i,e-1),this.setmask(t+i,e+1)},t.prototype.modnn=function(t){for(;255<=t;)t=((t-=255)>>8)+(255&t);return t},t.prototype.appendrs=function(t,e,i,r){for(var o,s=0;s>=1,this.framask[i+=t]},t.prototype.badruns=function(t){for(var e=0,i=0;i<=t;i++)5<=this.rlens[i]&&(e+=this.N1+this.rlens[i]-5);for(i=3;i=4*this.rlens[i]||3*this.rlens[i+3]>=4*this.rlens[i])&&(e+=this.N3);return e},t.prototype.toNum=function(t){return 0===t?1:0},t.prototype.applymask=function(t){switch(t){case 0:for(var e=0;e>1&1,i=0;i>o-12:e>>o)?(this.qrframe[5-c+this.width*(2-d+this.width-11)]=1,this.qrframe[2-d+this.width-11+this.width*(5-c)]=1):(this.setmask(5-c,2-d+this.width-11),this.setmask(2-d+this.width-11,5-c));for(d=0;d>4;this.strinbuf[2]|=255&n<<4,this.strinbuf[1]=n>>4,this.strinbuf[0]=64|n>>12}else{for(this.strinbuf[a+1]=0,this.strinbuf[a+2]=0;a--;)e=this.strinbuf[a],this.strinbuf[a+2]|=255&e<<4,this.strinbuf[a+1]=e>>4;this.strinbuf[1]|=255&n<<4,this.strinbuf[0]=64|n>>4}for(a=n+3-(h<10?1:0);a>=1)1&s&&(this.qrframe[this.width-1-S+8*this.width]=1,S<6?this.qrframe[8+this.width*S]=1:this.qrframe[8+this.width*(S+1)]=1);for(S=0;S<7;S++,s>>=1)1&s&&(this.qrframe[8+this.width*(this.width-7+S)]=1,S?this.qrframe[6-S+8*this.width]=1:this.qrframe[7+8*this.width]=1);return this.qrframe},t.prototype.badcheck=function(){for(var t=0,e=0,i=0,r=0,o=0,s=0,n=0,h=0;hthis.width*this.width;)l-=this.width*this.width,c++;t+=c*this.N4;for(a=0;ao+1?e[o+1]:-1,r=r.concat(function(t,e,i){var r=[],o=-1;if(charCompatible(t,i))5==i&&(-1==e?(o=100,i=4):charCompatible(e,i)||(i=charCompatible(e,3)?(o=101,3):(o=100,4)));else if(-1==e||charCompatible(e,i))o=98;else switch(i){case 3:o=100,i=4;break;case 4:o=101,i=3}return-1!=o?(r.push(o),r.push(codeValue(t))):5==i?r.push(codeValue(t,e)):r.push(codeValue(t)),s.currcs=i,r}(n,h,s.currcs));5==s.currcs&&o++}for(var a=r[0],l=1;l {
- // if (uni.cache.get('shopUserInfo') && uni.cache.get('shopId') == uni.cache.get('shopUserInfo').id) {
+ // if (uni.cache.get('shopUserInfo') && props.shopId||uni.cache.get('shopId') == uni.cache.get('shopUserInfo').id) {
// shopUserInfo = uni.cache.get('shopUserInfo');
// } else {
// let res = await this.api.shopUserInfo({
- // "shopId": uni.cache.get('shopId'),
+ // "shopId": props.shopId||uni.cache.get('shopId'),
// "userId": uni.cache.get('userInfo').id,
// })
// if (res.code == 0) {
@@ -192,7 +196,7 @@
url: uni.conf.baseUrl + '/account/user/common/upload',
filePath: avatarUrl,
header: {
- shopId: uni.cache.get('shopId'),
+ shopId: props.shopId||uni.cache.get('shopId'),
token: uni.cache.get('token') || '',
'content-type': 'multipart/form-data'
},
@@ -339,7 +343,7 @@
}
const submitForm={
// id: uni.cache.get('userInfo').id,
- // shopId: uni.cache.get('shopId'),
+ shopId: props.shopId||uni.cache.get('shopId'),
nickName: formInfo.nickName,
headImg: userHeadImg.value,
phone: formInfo.telephone,
@@ -348,7 +352,7 @@
}
await APIshopUser(submitForm)
let APIshopUserInfores = await APIshopUserInfo({
- shopId: uni.cache.get('shopId')
+ shopId: props.shopId||uni.cache.get('shopId')
})
uni.cache.set('orderVIP', APIshopUserInfores)
uni.cache.set('ordershopUserInfo', APIshopUserInfores.shopInfo)
diff --git a/user/exchange/components/confirm.vue b/user/exchange/components/confirm.vue
new file mode 100644
index 0000000..2901778
--- /dev/null
+++ b/user/exchange/components/confirm.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+ 确认信息
+
+
+
+ 兑换码包含内容如下:
+
+ 店铺
+ {{data.shopName}}
+
+
+ 名称
+ {{data.name}}
+
+
+ 优惠券
+ {{data.couponInfoList.map(item=>item.title+'*'+item.num).join('、')}}
+
+
+ 金额
+ {{data.amount}}
+
+
+ 取消
+ 确认
+
+
+
+
+
+
+
+
+
diff --git a/user/exchange/components/result.vue b/user/exchange/components/result.vue
new file mode 100644
index 0000000..c9597e7
--- /dev/null
+++ b/user/exchange/components/result.vue
@@ -0,0 +1,81 @@
+
+
+
+
+ {{ title }}
+
+ 确认
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/user/exchange/index.vue b/user/exchange/index.vue
new file mode 100644
index 0000000..013abd9
--- /dev/null
+++ b/user/exchange/index.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+ 兑换您的专属福利
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 兑换说明:
+
+ 1、请直接输入您获得的兑换码,点击立即兑换即可
+
+ 2、兑换码为一次性,兑换完成之后即失效
+
+ 3、兑换获得的奖励,将直接发送至您的账号,可直接前往余额或优惠券查看
+
+ 4、兑换码为赠品,不可转赠、不退不换
+
+ 5、兑换码需在有效期内完成兑换,过期即作废
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/user/static/duihuan.png b/user/static/duihuan.png
new file mode 100644
index 0000000..f83fb4e
Binary files /dev/null and b/user/static/duihuan.png differ
diff --git a/user/vip/buy-vip.vue b/user/vip/buy-vip.vue
index ede2699..d608abf 100644
--- a/user/vip/buy-vip.vue
+++ b/user/vip/buy-vip.vue
@@ -137,7 +137,7 @@
-
+
@@ -182,8 +182,9 @@
}
if (name == '周期福利') {
model.title = '周期福利'
+ let couponLen=memberLevel.value.cycleRewardCouponList?memberLevel.value.cycleRewardCouponList.length:0
modelContent.value =
- `每${memberLevel.value.cycleUnit}赠送${memberLevel.value.cycleRewardPoints}积分,${memberLevel.value.cycleRewardCouponList.length}张优惠券`
+ `每${memberLevel.value.cycleUnit}赠送${memberLevel.value.cycleRewardPoints}积分${couponLen?(','+couponLen+'张优惠券'):''}`
}
if (name == '会员说明') {
model.title = '会员说明'
diff --git a/user/vip/vip.vue b/user/vip/vip.vue
index 694b4a2..03f2904 100644
--- a/user/vip/vip.vue
+++ b/user/vip/vip.vue
@@ -13,7 +13,7 @@
{{ memberLevel ? memberLevel.name : '' }}
{{ memberLevel ? dayjs(memberLevel.endTime).format('YYYY-MM-DD') : '' }}到期
- {{ memberLevel ? memberLevel.memberName : '' }}
+ {{ memberLevel ? memberLevel.memberCircleName : '' }}
@@ -235,6 +235,27 @@
const res = await vipApi.config({
shopId: shopId
});
+
+ const res1 = await vipApi.detail({
+ shopId: shopId
+ });
+ if (res1) {
+ memberLevel.value = {
+ ...res1.memberLevel,
+ endTime: res1.endTime,
+ memberName: res1.memberName,
+ experience: res1.experience,
+ nextExperienceValue: res1.nextExperienceValue,
+ memberCircleName:res1.memberCircleName,
+ memberCircleReward:res1.memberCircleReward,
+ };
+ const total = res1.experience * 1 + res1.nextExperienceValue * 1;
+ const percent = ((res1.experience * 1) / total) * 100;
+ if (total <= 0 || res1.experience * 1 <= 0) {
+ expProgress.value = 100;
+ }
+ expProgress.value = percent;
+ }
if (res) {
res.memberLevel.cycleRewardCouponList = res.memberLevel.cycleRewardCouponList || []
res.memberLevel = res.memberLevel || {}
@@ -254,7 +275,7 @@
return true
}).map((v) => {
if (v.name == '赠送成长值' && res.memberLevel) {
- v.desc = `赠送${res.memberLevel.experienceValue}成长值`
+ v.desc = `赠送${memberLevel.value.memberCircleReward}成长值`
}
if (v.name == '优惠券') {
if (res.memberLevel.cycleRewardCouponList.length) {
@@ -270,10 +291,10 @@
return v;
});
tiaojian_menus = tiaojian_menus.filter(v => {
- if (!state.isCostRewardPoints && v.name == '消费送积分') {
+ if (!res.memberLevel.isCostRewardPoints && v.name == '消费送积分') {
return false
}
- if (!state.isCycleReward && v.name == '周期福利') {
+ if (!res.memberLevel.isCycleReward && v.name == '周期福利') {
return false
}
if (!res.memberLevel.cycleRewardCouponList || res.memberLevel.cycleRewardCouponList.length <=
@@ -301,24 +322,6 @@
menus.value = tiaojian_menus;
}
}
- const res1 = await vipApi.detail({
- shopId: shopId
- });
- if (res1) {
- memberLevel.value = {
- ...res1.memberLevel,
- endTime: res1.endTime,
- memberName: res1.memberName,
- experience: res1.experience,
- nextExperienceValue: res1.nextExperienceValue
- };
- const total = res1.experience * 1 + res1.nextExperienceValue * 1;
- const percent = ((res1.experience * 1) / total) * 100;
- if (total <= 0 || res1.experience * 1 <= 0) {
- expProgress.value = 100;
- }
- expProgress.value = percent;
- }
}
const nextName = computed(() => {
diff --git a/utils/address.js b/utils/address.js
new file mode 100644
index 0000000..6fa599f
--- /dev/null
+++ b/utils/address.js
@@ -0,0 +1,13 @@
+// 计算距离
+export const getDistance = (la1, lo1, la2, lo2) => { // 当前的纬度,当前的经度,接口拿到的纬度,接口拿到的经度
+ let La1 = la1 * Math.PI / 180.0;
+ let La2 = la2 * Math.PI / 180.0;
+ let La3 = La1 - La2;
+ let Lb3 = lo1 * Math.PI / 180.0 - lo2 * Math.PI / 180.0;
+ let distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(La3 / 2), 2) + Math.cos(La1) * Math.cos(La2) *
+ Math.pow(Math.sin(Lb3 / 2), 2)));
+ distance = distance * 6378.137;
+ distance = Math.round(distance * 10000) / 10000;
+ return distance;
+ }
+
\ No newline at end of file
diff --git a/utils/goods copy.ts b/utils/goods copy.ts
new file mode 100644
index 0000000..47713c4
--- /dev/null
+++ b/utils/goods copy.ts
@@ -0,0 +1,1710 @@
+import { BigNumber } from "bignumber.js";
+
+// 配置BigNumber精度
+BigNumber.set({
+ DECIMAL_PLACES: 2,
+ ROUNDING_MODE: BigNumber.ROUND_DOWN, // 向下取整,符合业务需求
+});
+
+/**
+ * 购物车订单价格计算公共库
+ * 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展
+ * 小数处理:使用bignumber.js确保精度,统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99)
+ * 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑
+ * 关键规则:
+ * - 所有优惠券均支持指定门槛商品(后端foods字段:空字符串=全部商品,ID字符串=指定商品ID)
+ * - 与限时折扣/会员价同享规则:开启则门槛计算含对应折扣,关闭则用原价/非会员价
+ * 字段说明:
+ * - BaseCartItem.id:购物车项ID(唯一标识购物车中的条目)
+ * - BaseCartItem.product_id:商品ID(唯一标识商品,用于优惠券/活动匹配)
+ * - BaseCartItem.skuData.id:SKU ID(唯一标识商品规格)
+ */
+
+// ============================ 1. 基础类型定义(核心修正:明确ID含义) ============================
+/** 商品类型枚举 */
+export enum GoodsType {
+ NORMAL = "normal", // 普通商品
+ WEIGHT = "weight", // 称重商品
+ GIFT = "gift", // 赠菜(继承普通商品逻辑,标记用)
+ EMPTY = "", // 空字符串类型(后端未返回时默认归类为普通商品)
+ PACKAGE = "package", // 打包商品(如套餐/预打包商品,按普通商品逻辑处理,可扩展特殊规则)
+}
+
+/** 优惠券计算结果类型(新增细分字段) */
+interface CouponResult {
+ deductionAmount: number; // 抵扣金额
+ excludedProductIds: string[]; // 不适用商品ID列表(注意:是商品ID,非购物车ID)
+ usedCoupon: Coupon | undefined; // 实际使用的优惠券
+ productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券等)
+ fullCouponDeduction: number; // 新增:满减优惠券抵扣
+}
+
+/** 兑换券计算结果类型(新增细分字段) */
+interface ExchangeCalculationResult {
+ deductionAmount: number;
+ excludedProductIds: string[]; // 不适用商品ID列表(商品ID)
+ productCouponDeduction: number; // 新增:兑换券属于商品券,同步记录
+}
+
+/** 优惠券类型枚举 */
+export enum CouponType {
+ FULL_REDUCTION = "full_reduction", // 满减券
+ DISCOUNT = "discount", // 折扣券
+ SECOND_HALF = "second_half", // 第二件半价券
+ BUY_ONE_GET_ONE = "buy_one_get_one", // 买一送一券
+ EXCHANGE = "exchange", // 商品兑换券
+}
+
+/** 后端返回的优惠券原始字段类型 */
+export interface BackendCoupon {
+ id?: number; // 自增主键(int64)
+ shopId?: number; // 店铺ID(int64)
+ syncId?: number; // 同步Id(int64)
+ type?: number; // 优惠券类型:1-满减券,2-商品兑换券,3-折扣券,4-第二件半价券,5-消费送券,6-买一送一券,7-固定价格券,8-免配送费券
+ name?: string; // 券名称
+ useShopType?: string; // 可用门店类型:only-仅本店;all-所有门店,custom-指定门店
+ useShops?: string; // 可用门店(逗号分隔字符串,如"1,2,3")
+ useType?: string; // 可使用类型:dine堂食/pickup自取/deliv配送/express快递
+ validType?: string; // 有效期类型:fixed(固定时间),custom(自定义时间)
+ validDays?: number; // 有效期(天)
+ validStartTime?: string; // 有效期开始时间(如"2024-01-01 00:00:00")
+ validEndTime?: string; // 有效期结束时间
+ daysToTakeEffect?: number; // 隔天生效
+ useDays?: string; // 可用周期(如"周一,周二")
+ useTimeType?: string; // 可用时间段类型:all-全时段,custom-指定时段
+ useStartTime?: string; // 可用开始时间(每日)
+ useEndTime?: string; // 可用结束时间(每日)
+ getType?: string; // 发放设置:不可自行领取/no,可领取/yes
+ getMode?: string; // 用户领取方式
+ giveNum?: number; // 总发放数量,-10086为不限量
+ getUserType?: string; // 可领取用户:全部/all,新用户一次/new,仅会员/vip
+ getLimit?: number; // 每人领取限量,-10086为不限量
+ useLimit?: number; // 每人每日使用限量,-10086为不限量
+ discountShare?: number; // 与限时折扣同享:0-否,1-是
+ vipPriceShare?: number; // 与会员价同享:0-否,1-是
+ ruleDetails?: string; // 附加规则说明
+ status?: number; // 状态:0-禁用,1-启用
+ useNum?: number; // 已使用数量
+ leftNum?: number; // 剩余数量
+ foods?: string; // 指定门槛商品(逗号分隔字符串,如"101,102",此处为商品ID)
+ fullAmount?: number; // 使用门槛:满多少金额(元)
+ discountAmount?: number; // 使用门槛:减多少金额(元)
+ discountRate?: number; // 折扣%(如90=9折)
+ maxDiscountAmount?: number; // 可抵扣最大金额(元)
+ useRule?: string; // 使用规则:price_asc-价格低到高,price_desc-高到低
+ discountNum?: number; // 抵扣数量
+ otherCouponShare?: number; // 与其它优惠共享:0-否,1-是
+ createTime?: string; // 创建时间
+ updateTime?: string; // 更新时间
+}
+
+/** 营销活动类型枚举 */
+export enum ActivityType {
+ TIME_LIMIT_DISCOUNT = "time_limit_discount", // 限时折扣
+}
+
+/** 基础购物车商品项(核心修正:新增product_id,明确各ID含义) */
+export interface BaseCartItem {
+ id: string | number; // 购物车ID(唯一标识购物车中的条目,如购物车项主键)
+ product_id: string | number; // 商品ID(唯一标识商品,用于优惠券/活动匹配,必选)
+ salePrice: number; // 商品原价(元)
+ number: number; // 商品数量
+ product_type: GoodsType; // 商品类型
+ is_temporary?: boolean; // 是否临时菜(默认false)
+ is_gift?: boolean; // 是否赠菜(默认false)
+ returnNum?: number; // 退货数量(历史订单用,默认0)
+ memberPrice?: number; // 商品会员价(元,优先级:商品会员价 > 会员折扣)
+ discountSaleAmount?: number; // 商家改价后单价(元,优先级最高)
+ packFee?: number; // 单份打包费(元,默认0)
+ packNumber?: number; // 堂食打包数量(默认0)
+ activityInfo?: {
+ // 商品参与的营销活动(如限时折扣)
+ type: ActivityType;
+ discountRate: number; // 折扣率(如0.8=8折)
+ vipPriceShare: boolean; // 是否与会员优惠同享(默认false)
+ };
+ skuData?: {
+ // SKU扩展数据(可选)
+ id: string | number; // SKU ID(唯一标识商品规格,如颜色/尺寸)
+ memberPrice?: number; // SKU会员价
+ salePrice?: number; // SKU原价
+ };
+}
+
+/** 基础优惠券接口(所有券类型继承,包含统一门槛商品字段) */
+export interface BaseCoupon {
+ id: string | number; // 优惠券ID
+ type: CouponType; // 工具库字符串枚举(由后端couponType转换)
+ name: string; // 对应后端title
+ available: boolean; // 基于BackendCoupon字段计算的可用性
+ useShopType?: string; // only-仅本店;all-所有门店,custom-指定门店
+ useShops: string[]; // 可用门店ID列表
+ discountShare: boolean; // 与限时折扣同享:0-否,1-是(后端字段转换为布尔值)
+ vipPriceShare: boolean; // 与会员价同享:0-否,1-是(后端字段转换为布尔值)
+ useType?: string[]; // 可使用类型:dine堂食/pickup自取/deliv配送/express快递
+ isValid: boolean; // 是否在有效期内
+ discountAmount?: number; // 减免金额 (满减券有)
+ fullAmount?: number; // 使用门槛:满多少金额
+ maxDiscountAmount?: number; // 可抵扣最大金额 元
+ applicableProductIds: string[]; // 门槛商品ID列表(空数组=全部商品,非空=指定商品ID)
+}
+
+/** 满减券(适配后端字段) */
+export interface FullReductionCoupon extends BaseCoupon {
+ type: CouponType.FULL_REDUCTION;
+ fullAmount: number; // 对应后端fullAmount(满减门槛)
+ discountAmount: number; // 对应后端discountAmount(减免金额)
+ maxDiscountAmount?: number; // 对应后端maxDiscountAmount(最大减免)
+}
+
+/** 折扣券(适配后端字段) */
+export interface DiscountCoupon extends BaseCoupon {
+ type: CouponType.DISCOUNT;
+ discountRate: number; // 后端discountRate(%)转小数(如90→0.9)
+ maxDiscountAmount: number; // 对应后端maxDiscountAmount(最大减免)
+}
+
+/** 第二件半价券(适配后端字段) */
+export interface SecondHalfPriceCoupon extends BaseCoupon {
+ type: CouponType.SECOND_HALF;
+ maxUseCountPerOrder?: number; // 对应后端useLimit(-10086=不限)
+}
+
+/** 买一送一券(适配后端字段) */
+export interface BuyOneGetOneCoupon extends BaseCoupon {
+ type: CouponType.BUY_ONE_GET_ONE;
+ maxUseCountPerOrder?: number; // 对应后端useLimit(-10086=不限)
+}
+
+/** 商品兑换券(适配后端字段) */
+export interface ExchangeCoupon extends BaseCoupon {
+ type: CouponType.EXCHANGE;
+ deductCount: number; // 对应后端discountNum(抵扣数量)
+ sortRule: "low_price_first" | "high_price_first"; // 后端useRule转换
+}
+
+/** 所有优惠券类型联合 */
+export type Coupon =
+ | FullReductionCoupon
+ | DiscountCoupon
+ | SecondHalfPriceCoupon
+ | BuyOneGetOneCoupon
+ | ExchangeCoupon;
+
+/** 营销活动配置(如限时折扣,applicableProductIds为商品ID列表) */
+export interface ActivityConfig {
+ type: ActivityType;
+ applicableProductIds?: string[]; // 适用商品ID列表(与BaseCartItem.product_id匹配)
+ discountRate: number; // 折扣率(如0.8=8折)
+ vipPriceShare: boolean; // 是否与会员优惠同享
+}
+
+/** 积分抵扣规则 */
+export interface PointDeductionRule {
+ pointsPerYuan: number; // X积分=1元(如100=100积分抵1元)
+ maxDeductionAmount?: number; // 最大抵扣金额(元,默认不限)
+}
+
+/** 餐位费配置 */
+export interface SeatFeeConfig {
+ pricePerPerson: number; // 每人餐位费(元)
+ personCount: number; // 用餐人数(默认1)
+ isEnabled: boolean; // 是否启用餐位费(默认false)
+}
+/** 商家减免类型枚举 */
+export enum MerchantReductionType {
+ FIXED_AMOUNT = "fixed_amount", // 固定金额减免(如直接减 10 元)
+ DISCOUNT_RATE = "discount_rate", // 比例折扣减免(如打 9 折,即减免 10%)
+}
+
+/** 商家减免配置(新增,替代原单一金额字段) */
+export interface MerchantReductionConfig {
+ type: MerchantReductionType; // 减免类型(二选一)
+ fixedAmount?: number; // 固定减免金额(元,仅 FIXED_AMOUNT 生效,≥0)
+ discountRate?: number; // 折扣率(%,仅 DISCOUNT_RATE 生效,0-100,如 90 代表 9 折)
+}
+/**商家霸王餐配置 */
+export interface FreeDineConfig {
+ enable: boolean; //是否开启
+ rechargeThreshold: number; //订单满多少元可以使用
+ rechargeTimes: number; //充值多少倍免单
+ withCoupon: boolean; //与优惠券同享
+ withPoints: boolean; //与积分同享
+ useType?: string[]; //使用类型 dine-in店内 takeout 自取 post快递,takeaway外卖
+ useShopType?: string; //all 全部 part部分
+ shopIdList?: number[]; //可用门店id
+}
+
+//限时折扣配置
+export interface TimeLimitDiscountConfig {
+ /**
+ * 折扣优先级 limit-time/vip-price
+ */
+ discountPriority: string;
+ /**
+ * 折扣% 范围1-99
+ */
+ discountRate: number;
+ /**
+ * 参与商品
+ */
+ foods: string;
+ /**
+ * 参与商品 1全部 2部分
+ */
+ foodType: number;
+ /**
+ * 自增主键
+ */
+ id: number;
+ /**
+ * 店铺ID
+ */
+ shopId: number;
+ /**
+ * 可使用类型:堂食 dine-in 外带 take-out 外卖 take-away 配送 post
+ */
+ useType: string;
+ [property: string]: any;
+}
+
+//用户信息
+interface ShopUserInfo {
+ isVip: number | null; //是否会员
+ discount: number | null; //用户折扣
+ isMemberPrice: number | null; //会员折扣与会员价是否同时使用
+}
+/** 订单额外费用配置 */
+export interface OrderExtraConfig {
+ // merchantReduction: number; // 商家减免金额(元,默认0)
+ // 替换原单一金额字段,支持两种减免形式
+ merchantReduction: MerchantReductionConfig;
+ additionalFee: number; // 附加费(元,如余额充值、券包,默认0)
+ pointDeductionRule: PointDeductionRule; // 积分抵扣规则
+ seatFeeConfig: SeatFeeConfig; // 餐位费配置
+ currentStoreId: string; // 当前门店ID(用于验证优惠券适用门店)
+ userPoints: number; // 用户当前积分(用于积分抵扣)
+ isMember: boolean; // 用户是否会员(用于会员优惠)
+ memberDiscountRate?: number; // 会员折扣率(如0.95=95折,无会员价时用)
+ newUserDiscount?: number; // 新用户减免金额(元,默认0)
+ fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构)
+ currentDinnerType: "dine-in" | "take-out" | "take-away" | "post"; // 当前就餐类型(匹配useType)
+ isFreeDine?: boolean; //是否霸王餐
+ freeDineConfig?: FreeDineConfig;
+ limitTimeDiscount?: TimeLimitDiscountConfig; //限时折扣
+ shopUserInfo: ShopUserInfo; // 用户信息
+}
+
+/** 订单费用汇总(修改:补充商家减免类型和明细) */
+export interface OrderCostSummary {
+ // 商品总件数
+ goodsTotal: number;
+ totalDiscountAmount: number;
+ goodsRealAmount: number; // 商品真实原价总和
+ goodsOriginalAmount: number; // 商品原价总和
+ goodsDiscountAmount: number; // 商品折扣金额
+ couponDeductionAmount: number; // 优惠券总抵扣
+ productCouponDeduction: number; // 商品优惠券抵扣
+ fullCouponDeduction: number; // 满减优惠券抵扣
+ pointDeductionAmount: number; // 积分抵扣金额
+ seatFee: number; // 餐位费
+ packFee: number; // 打包费
+ scoreMaxMoney: number; // 积分最大可抵扣金额
+ // 新增:商家减免明细
+ merchantReduction: {
+ type: MerchantReductionType; // 实际使用的减免类型
+ originalConfig: MerchantReductionConfig; // 原始配置(便于前端展示)
+ actualAmount: number; // 实际减免金额(计算后的值,≥0)
+ };
+ additionalFee: number; // 附加费
+ finalPayAmount: number; // 最终实付金额
+ couponUsed?: Coupon; // 实际使用的优惠券
+ pointUsed: number; // 实际使用的积分
+ newUserDiscount: number; // 新用户减免金额(元,默认0)
+ dinnerType?: "dine-in" | "take-out"; // 就餐类型(堂食/自取/配送/快递)
+ config: OrderExtraConfig; // 订单额外费用配置
+ //满减活动
+ fullReduction: {
+ usedFullReductionActivityFullAmount: number; // 计算出的满减活动的门槛金额
+ usedActivity?: FullReductionActivity; // 实际使用的满减活动
+ usedThreshold?: FullReductionThreshold; // 实际使用的满减阈值(多门槛中选最优)
+ actualAmount: number; // 满减实际减免金额(元)
+ };
+ vipDiscountAmount: number; //会员折扣减免金额
+ // 订单原支付金额
+ orderOriginFinalPayAmount: number; //订单原金额(包含打包费+餐位费)
+}
+
+/** 满减活动阈值(单条满减规则:满X减Y)- 对应 MkDiscountThresholdInsertGroupDefaultGroup */
+export interface FullReductionThreshold {
+ activityId?: number; // 关联满减活动ID
+ fullAmount?: number; // 满多少金额(元,必填)
+ discountAmount?: number; // 减多少金额(元,必填)
+}
+
+/** 满减活动主表 - 对应 Request 接口(后端真实字段) */
+export interface FullReductionActivity {
+ id?: number; // 自增主键(后端字段:id)
+ shopId?: number; // 店铺ID(后端字段:shopId)
+ status?: number; // 活动状态:1=未开始,2=进行中,3=已结束(后端字段:status)
+ sort?: number; // 排序值(越大优先级越高,后端字段:sort)
+ createTime?: string; // 创建时间(后端字段:createTime,格式如"2025-10-14 13:56:07")
+ updateTime?: string; // 最新修改时间(后端字段:updateTime,用于优先级排序)
+ validStartTime?: string; // 有效期开始时间(后端字段:validStartTime,格式如"2025-10-14")
+ validEndTime?: string; // 有效期结束时间(后端字段:validEndTime,格式如"2025-12-14")
+ useType?: string; // 可使用类型(后端字段:useType,如"dine,pickup,deliv,express")
+ useDays?: string; // 可用周期(后端字段:useDays,如"周一,周二,周三,周四,周五,周六,周日")
+ useTimeType?: string; // 可用时间段类型(后端字段:useTimeType,all=全时段,custom=指定时段)
+ useStartTime?: string; // 每日可用开始时间(后端字段:useStartTime,如"09:00:00",仅custom时有效)
+ useEndTime?: string; // 每日可用结束时间(后端字段:useEndTime,如"22:00:00",仅custom时有效)
+ couponShare?: number; // 与优惠券同享:0=否,1=是(后端字段:couponShare)
+ discountShare?: number; // 与限时折扣同享:0=否,1=是(后端字段:discountShare)
+ vipPriceShare?: number; // 与会员价同享:0=否,1=是(后端字段:vipPriceShare)
+ pointsShare?: number; // 与积分抵扣同享:0=否,1=是(后端字段:pointsShare)
+ thresholds?: FullReductionThreshold[]; // 满减阈值列表(多门槛,后端字段:thresholds)
+ isDel?: boolean; // 是否删除:0=否,1=是(后端字段:isDel,默认false)
+}
+
+// 辅助枚举:星期映射(用于useDays校验)
+const WEEKDAY_MAP = {
+ 周一: 1,
+ 周二: 2,
+ 周三: 3,
+ 周四: 4,
+ 周五: 5,
+ 周六: 6,
+ 周日: 0, // JS中getDay()返回0=周日
+};
+
+/**
+ * 辅助:校验当前时间是否在活动的「每日可用时段」内
+ * @param activity 满减活动
+ * @param currentTime 当前时间
+ * @returns 是否在时段内
+ */
+function isInDailyTimeRange(
+ activity: FullReductionActivity,
+ currentTime: Date
+): boolean {
+ // 全时段无需校验
+ if (activity.useTimeType === "all") return true;
+ // 无时段配置则不通过
+ if (!activity.useStartTime || !activity.useEndTime) return false;
+
+ const [startHour, startMinute] = activity.useStartTime.split(":").map(Number);
+ const [endHour, endMinute] = activity.useEndTime.split(":").map(Number);
+ const currentHour = currentTime.getHours();
+ const currentMinute = currentTime.getMinutes();
+
+ // 转换为分钟数比较
+ const startTotalMin = startHour * 60 + startMinute;
+ const endTotalMin = endHour * 60 + endMinute;
+ const currentTotalMin = currentHour * 60 + currentMinute;
+
+ // 处理跨天场景(如23:00-02:00)
+ if (startTotalMin <= endTotalMin) {
+ return currentTotalMin >= startTotalMin && currentTotalMin <= endTotalMin;
+ } else {
+ return currentTotalMin >= startTotalMin || currentTotalMin <= endTotalMin;
+ }
+}
+
+/**
+ * 辅助:校验当前时间是否在活动的「可用周期」内(如周一至周日)
+ * @param activity 满减活动
+ * @param currentTime 当前时间
+ * @returns 是否在周期内
+ */
+function isInWeeklyCycle(
+ activity: FullReductionActivity,
+ currentTime: Date
+): boolean {
+ // 无周期配置则不通过
+ if (!activity.useDays) return false;
+ const currentWeekday = currentTime.getDay(); // 0=周日,1=周一...6=周六
+ const allowedWeekdays = activity.useDays
+ .split(",")
+ .map((day) => WEEKDAY_MAP[day as keyof typeof WEEKDAY_MAP]);
+ return allowedWeekdays.includes(currentWeekday);
+}
+
+/**
+ * 辅助:校验当前就餐类型是否在活动的「可用类型」内(如堂食/自取)
+ * @param activity 满减活动
+ * @param currentDinnerType 当前就餐类型
+ * @returns 是否匹配
+ */
+function isDinnerTypeMatch(
+ activity: FullReductionActivity,
+ currentDinnerType: string
+): boolean {
+ if (!activity.useType) return false;
+ const allowedTypes = activity.useType.split(",");
+ //满减活动的就餐类型和当前券类型字段值不一样,暂时返回true
+ return true;
+}
+
+//判断商品是否可以使用限时折扣
+export function returnCanUseLimitTimeDiscount(
+ goods: BaseCartItem,
+ limitTimeDiscount: TimeLimitDiscountConfig | null | undefined,
+ useVipPrice: boolean,
+ idKey = "product_id"
+) {
+ if (!limitTimeDiscount || !limitTimeDiscount.id) {
+ return false;
+ }
+ const canUseFoods = (limitTimeDiscount.foods || "").split(",");
+ const goodsCanUse =
+ limitTimeDiscount.foodType == 1 || canUseFoods.includes("" + goods[idKey]);
+ if (!goodsCanUse) {
+ return false;
+ }
+ if (limitTimeDiscount.discountPriority == "limit-time") {
+ return true;
+ }
+ if (limitTimeDiscount.discountPriority == "vip-price") {
+ if (!useVipPrice) {
+ return true;
+ }
+ if (useVipPrice && goods.hasOwnProperty("memberPrice")) {
+ if (goods.memberPrice && goods.memberPrice * 1 <= 0) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function returnMemberPrice(useVipPrice: boolean, goods: BaseCartItem) {
+ if (useVipPrice) {
+ return goods.memberPrice || goods.salePrice;
+ } else {
+ return goods.salePrice;
+ }
+}
+
+/**
+ * 返回商品限时折扣价格
+ */
+function returnLimitPrice(
+ goods: BaseCartItem,
+ limitTimeDiscount: TimeLimitDiscountConfig | null | undefined,
+ useVipPrice
+) {
+ if (!limitTimeDiscount) {
+ return 0;
+ }
+ const discountRate = new BigNumber(limitTimeDiscount.discountRate).dividedBy(
+ 100
+ );
+
+ const canuseLimit = returnCanUseLimitTimeDiscount(
+ goods,
+ limitTimeDiscount,
+ useVipPrice
+ );
+ if (canuseLimit) {
+ //可以使用限时折扣
+ if (limitTimeDiscount.discountPriority == "limit-time") {
+ //限时价优先
+ const result = BigNumber(goods.salePrice)
+ .times(discountRate)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ return result;
+ }
+ if (limitTimeDiscount.discountPriority == "vip-price") {
+ //会员价优先
+ if (useVipPrice && goods.memberPrice && goods.memberPrice * 1 > 0) {
+ //使用会员价
+ return returnMemberPrice(useVipPrice, goods);
+ } else {
+ //不使用会员价
+ const result = BigNumber(goods.salePrice)
+ .times(discountRate)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ return result;
+ }
+ }
+ } else {
+ //不可以使用限时折扣
+ //会员价优先
+ if (useVipPrice) {
+ //使用会员价
+ return returnMemberPrice(useVipPrice, goods);
+ } else {
+ return goods.salePrice;
+ }
+ }
+}
+
+/**
+ * 计算商品计算门槛时的金额
+ */
+
+export function returnCalcPrice(
+ goods: BaseCartItem,
+ fullReductionActivitie: FullReductionActivity | undefined,
+ limitTimeDiscount: TimeLimitDiscountConfig | null | undefined,
+ useVipPrice: boolean,
+ idKey = "product_id"
+) {
+ if (goods.discountSaleAmount && goods.discountSaleAmount * 1 > 0) {
+ return goods.salePrice;
+ }
+ //限时折扣和满减活动都有
+ if (fullReductionActivitie && limitTimeDiscount) {
+ if (
+ fullReductionActivitie.discountShare == 1 &&
+ fullReductionActivitie.vipPriceShare == 1
+ ) {
+ //与限时折扣同享,与会员价不同享
+ return returnLimitPrice(goods, limitTimeDiscount, useVipPrice);
+ }
+ if (
+ fullReductionActivitie.discountShare != 1 &&
+ fullReductionActivitie.vipPriceShare == 1
+ ) {
+ //与限时折扣不同享,与会员价同享
+ return returnMemberPrice(useVipPrice, goods);
+ }
+ if (fullReductionActivitie.vipPriceShare != 1) {
+ //与会员价不同享
+ return goods.salePrice;
+ }
+ return goods.salePrice;
+ }
+ //只有满减活动
+ if (fullReductionActivitie) {
+ if (fullReductionActivitie.vipPriceShare == 1) {
+ return returnMemberPrice(useVipPrice, goods);
+ } else {
+ return goods.salePrice;
+ }
+ }
+ //只有限时折扣
+ if (limitTimeDiscount) {
+ return returnLimitPrice(goods, limitTimeDiscount, useVipPrice);
+ }
+
+ if (useVipPrice) {
+ return returnMemberPrice(useVipPrice, goods);
+ }
+ return goods.salePrice;
+}
+
+/**
+ * 计算满减活动门槛
+ */
+export function calcFullReductionActivityFullAmount(
+ goodsList: BaseCartItem[],
+ fullReductionActivitie: FullReductionActivity | undefined,
+ limitTimeDiscount: TimeLimitDiscountConfig | null | undefined,
+ useVipPrice: boolean,
+ seatFee: number,
+ packFee: number
+): number {
+ if (!fullReductionActivitie) {
+ return 0;
+ }
+ let amount = 0;
+ for (let goods of goodsList) {
+ const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+ if (goods.is_temporary || goods.is_gift || availableNum <= 0) {
+ //临时菜,赠菜,数量<=0的商品不计算
+ continue;
+ }
+ const calcPrice = returnCalcPrice(
+ goods,
+ fullReductionActivitie,
+ limitTimeDiscount,
+ useVipPrice,
+ "product_id"
+ );
+ if (calcPrice !== undefined) {
+ amount += calcPrice * availableNum;
+ }
+ }
+ return amount + seatFee + packFee;
+ console.log("amount", amount);
+}
+
+/**
+ * 筛选最优满减活动(对齐后端逻辑:状态→时间→周期→时段→就餐类型→排序→修改时间)
+ * @param activities 后端返回的满减活动列表
+ * @param currentShopId 当前店铺ID
+ * @param currentDinnerType 当前就餐类型(dine/pickup等)
+ * @param currentTime 当前时间(默认当前时间)
+ * @returns 最优满减活动(无符合条件则返回undefined)
+ */
+export function filterOptimalFullReductionActivity(
+ activities: FullReductionActivity[],
+ currentShopId: number,
+ currentDinnerType: string,
+ currentTime: Date = new Date()
+): FullReductionActivity | undefined {
+ if (!activities || !activities.length) return undefined;
+ // 第一步:基础筛选(未删除+当前店铺+活动进行中+就餐类型匹配)
+ const baseEligible = activities.filter((activity) => {
+ return (
+ activity.isDel !== true && // 未删除
+ // activity.shopId === currentShopId && // 当前店铺
+ // activity.status === 2 && // 状态=2(进行中)
+ isDinnerTypeMatch(activity, currentDinnerType) && // 就餐类型匹配
+ activity.thresholds?.length // 至少有一个满减阈值
+ );
+ });
+ console.log("baseEligible", baseEligible);
+
+ if (!baseEligible.length) return undefined;
+
+ // 第二步:时间筛选(有效期内+周期内+时段内)
+ const timeEligible = baseEligible.filter((activity) => {
+ // 1. 校验有效期(validStartTime ~ validEndTime)
+ if (activity.useTimeType == "all") {
+ return true;
+ }
+ if (!activity.validStartTime || !activity.validEndTime) return false;
+ const startDate = new Date(activity.validStartTime);
+ const endDate = new Date(activity.validEndTime);
+ // 处理有效期结束日期的23:59:59
+ endDate.setHours(23, 59, 59, 999);
+ if (currentTime < startDate || currentTime > endDate) return false;
+
+ // 2. 校验可用周期(如周一至周日)
+ if (!isInWeeklyCycle(activity, currentTime)) return false;
+
+ // 3. 校验每日可用时段(如09:00-22:00)
+ if (!isInDailyTimeRange(activity, currentTime)) return false;
+
+ return true;
+ });
+
+ if (!timeEligible.length) return undefined;
+
+ // 第三步:按优先级排序(需求规则)
+ return timeEligible.sort((a, b) => {
+ // 1. 先比排序值:排序值大的优先
+ if ((a.sort || 0) !== (b.sort || 0)) {
+ return (b.sort || 0) - (a.sort || 0);
+ }
+ // 2. 再比修改时间:最新修改的优先(时间戳降序)
+ const aUpdateTime = a.updateTime ? new Date(a.updateTime).getTime() : 0;
+ const bUpdateTime = b.updateTime ? new Date(b.updateTime).getTime() : 0;
+ return bUpdateTime - aUpdateTime;
+ })[0]; // 取最优活动
+}
+/**
+ * 折扣率格式化:后端discountRate(%)→ 工具库小数(如90→0.9)
+ */
+export function formatDiscountRate(backendDiscountRate?: number): number {
+ if (!backendDiscountRate || backendDiscountRate <= 0) return 1; // 默认无折扣(1=100%)
+ // 后端若为百分比(如90=9折),除以100;若已为小数(如0.9)直接返回
+ return backendDiscountRate >= 1
+ ? backendDiscountRate / 100
+ : backendDiscountRate;
+}
+
+/**
+ * 统一小数处理:舍去小数点后两位以后的数字(解决浮点数精度偏差)
+ * 如 10.129 → 10.12,1.01 → 1.01,1.0100000001 → 1.01
+ * @param num 待处理数字
+ * @returns 处理后保留两位小数的数字
+ */
+export function truncateToTwoDecimals(num: number | string): number {
+ return new BigNumber(num).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber();
+}
+
+/**
+ * 判断商品是否为临时菜(临时菜不参与优惠券门槛和折扣计算)
+ * @param goods 商品项
+ * @returns 是否临时菜
+ */
+export function isTemporaryGoods(goods: BaseCartItem): boolean {
+ return !!goods.is_temporary;
+}
+
+/**
+ * 判断商品是否为赠菜(赠菜不计入优惠券活动,但计打包费)
+ * @param goods 商品项
+ * @returns 是否赠菜
+ */
+export function isGiftGoods(goods: BaseCartItem): boolean {
+ return !!goods.is_gift;
+}
+/**
+ * 判断可用类型是否可用
+ */
+export function useTypeCanUse(useType: string[]) {
+ const arr = ["all", "dine-in", "take-out", "take-away", "post"];
+ return useType.some((item) => arr.includes(item));
+}
+
+/**
+ * 计算单个商品的会员价(优先级:SKU会员价 > 商品会员价 > 会员折扣率)
+ * @param goods 商品项
+ * @param isMember 是否会员
+ * @param memberDiscountRate 会员折扣率(如0.95=95折)
+ * @returns 会员价(元)
+ */
+export function calcMemberPrice(
+ goods: BaseCartItem,
+ isMember: boolean,
+ memberDiscountRate?: number
+): number {
+ if (!isMember) return truncateToTwoDecimals(goods.salePrice);
+
+ // 优先级:SKU会员价 > 商品会员价 > 商品原价(无会员价时用会员折扣)
+ const basePrice =
+ goods.skuData?.memberPrice ?? goods.memberPrice ?? goods.salePrice;
+
+ // 仅当无SKU会员价、无商品会员价时,才应用会员折扣率
+ if (memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice) {
+ return truncateToTwoDecimals(
+ new BigNumber(basePrice).multipliedBy(memberDiscountRate).toNumber()
+ );
+ }
+
+ return truncateToTwoDecimals(basePrice);
+}
+
+/**
+ * 过滤可参与优惠券计算的商品(排除临时菜、赠菜、已用兑换券的商品)
+ * @param goodsList 商品列表
+ * @param excludedProductIds 需排除的商品ID列表(商品ID,非购物车ID)
+ * @returns 可参与优惠券计算的商品列表
+ */
+export function filterCouponEligibleGoods(
+ goodsList: BaseCartItem[],
+ excludedProductIds: string[] = []
+): BaseCartItem[] {
+ return goodsList.filter(
+ (goods) =>
+ !isTemporaryGoods(goods) &&
+ !isGiftGoods(goods) &&
+ !excludedProductIds.includes(String(goods.product_id)) // 核心修正:用商品ID排除
+ );
+}
+
+/**
+ * 统一筛选门槛商品的工具函数(所有券类型复用,按商品ID匹配)
+ * @param baseEligibleGoods 基础合格商品(已排除临时菜/赠菜/已抵扣商品)
+ * @param applicableProductIds 优惠券指定的门槛商品ID数组
+ * @returns 最终参与优惠券计算的商品列表
+ */
+export function filterThresholdGoods(
+ baseEligibleGoods: BaseCartItem[],
+ applicableProductIds: string[]
+): BaseCartItem[] {
+ // 空数组=全部基础合格商品;非空=仅商品ID匹配的商品(转字符串兼容类型)
+ return applicableProductIds.length === 0
+ ? baseEligibleGoods
+ : baseEligibleGoods.filter((goods) =>
+ applicableProductIds.includes(String(goods.product_id))
+ ); // 核心修正:用商品ID匹配
+}
+
+/**
+ * 商品排序(用于商品兑换券:按价格/数量/加入顺序排序,按商品ID分组去重)
+ * @param goodsList 商品列表
+ * @param sortRule 排序规则(low_price_first/high_price_first)
+ * @param cartOrder 商品加入购物车的顺序(key=购物车ID,value=加入时间戳)
+ * @returns 排序后的商品列表
+ */
+export function sortGoodsForCoupon(
+ goodsList: BaseCartItem[],
+ sortRule: "low_price_first" | "high_price_first",
+ cartOrder: Record = {}
+): BaseCartItem[] {
+ return [...goodsList].sort((a, b) => {
+ // 1. 按商品单价排序(优先级最高)
+ const priceA = a.skuData?.salePrice ?? a.salePrice;
+ const priceB = b.skuData?.salePrice ?? b.salePrice;
+ if (priceA !== priceB) {
+ return sortRule === "low_price_first" ? priceA - priceB : priceB - priceA;
+ }
+
+ // 2. 同价格按商品数量排序(降序,多的优先)
+ if (a.number !== b.number) {
+ return b.number - a.number;
+ }
+
+ // 3. 同价格同数量按加入购物车顺序(早的优先,用购物车ID匹配)
+ const orderA = cartOrder[String(a.id)] ?? Infinity;
+ const orderB = cartOrder[String(b.id)] ?? Infinity;
+ return orderA - orderB;
+ });
+}
+
+/**
+ * 计算优惠券门槛金额(根据同享规则,按商品ID匹配限时折扣)
+ * @param eligibleGoods 可参与优惠券的商品列表(已过滤临时菜/赠菜)
+ * @param coupon 优惠券(含discountShare/vipPriceShare配置)
+ * @param config 订单配置(会员信息)
+ * @param activities 全局营销活动(限时折扣,applicableProductIds为商品ID)
+ * @returns 满足优惠券门槛的金额基数
+ */
+export function calcCouponThresholdAmount(
+ eligibleGoods: BaseCartItem[],
+ coupon: BaseCoupon,
+ config: Pick,
+ activities: ActivityConfig[] = []
+): number {
+ let total = new BigNumber(0);
+
+ for (const goods of eligibleGoods) {
+ const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+ if (availableNum <= 0) continue;
+
+ // 1. 基础金额:默认用商品原价(SKU原价优先)
+ const basePrice = new BigNumber(
+ goods.skuData?.salePrice ?? goods.salePrice
+ );
+ let itemAmount = basePrice.multipliedBy(availableNum);
+
+ // 2. 处理「与会员价/会员折扣同享」规则:若开启,用会员价计算
+ if (coupon.vipPriceShare) {
+ const memberPrice = new BigNumber(
+ calcMemberPrice(goods, config.isMember, config.memberDiscountRate)
+ );
+ itemAmount = memberPrice.multipliedBy(availableNum);
+ }
+
+ // 3. 处理「与限时折扣同享」规则:若开启,叠加限时折扣(按商品ID匹配活动)
+ if (coupon.discountShare) {
+ const activity =
+ goods.activityInfo ??
+ activities.find(
+ (act) =>
+ act.type === ActivityType.TIME_LIMIT_DISCOUNT &&
+ (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动
+ );
+
+ if (activity) {
+ itemAmount = itemAmount.multipliedBy(activity.discountRate); // 叠加限时折扣
+ }
+ }
+
+ total = total.plus(itemAmount);
+ }
+
+ return truncateToTwoDecimals(total.toNumber());
+}
+
+// ============================ 3. 商品核心价格计算(核心修正:按商品ID匹配营销活动) ============================
+/**
+ * 计算单个商品的实际单价(整合商家改价、会员优惠、营销活动折扣,按商品ID匹配活动)
+ * @param goods 商品项
+ * @param config 订单额外配置(含会员、活动信息)
+ * @returns 单个商品实际单价(元)
+ */
+export function calcSingleGoodsRealPrice(
+ goods: BaseCartItem,
+ config: Pick<
+ OrderExtraConfig,
+ "isMember" | "memberDiscountRate" | "limitTimeDiscount"
+ >
+): number {
+ const { isMember, memberDiscountRate, limitTimeDiscount: activity } = config;
+ console.log("activity", activity);
+
+ //如果是增菜价格为0
+ if (goods.is_gift) {
+ return 0;
+ }
+
+ // 1. 优先级1:商家改价(改价后单价>0才生效)
+ if (goods.discountSaleAmount && goods.discountSaleAmount > 0) {
+ return truncateToTwoDecimals(goods.discountSaleAmount);
+ }
+
+ // 2. 优先级2:会员价(含会员折扣率,SKU会员价优先)
+ const memberPrice = new BigNumber(
+ calcMemberPrice(goods, isMember, memberDiscountRate)
+ );
+
+ // 3. 优先级3:营销活动折扣(如限时折扣,需按商品ID匹配活动)
+ let isActivityApplicable = false;
+ if (activity) {
+ if (activity.foodType == 1) {
+ isActivityApplicable = true;
+ } else {
+ const canUseGoods = activity.foods?.split(",") || [];
+ if (canUseGoods.find((v) => v == String(goods.product_id))) {
+ isActivityApplicable = true;
+ }
+ }
+ }
+ if (!activity || !isActivityApplicable) {
+ return memberPrice.toNumber();
+ }
+ console.log("isMember", isMember);
+ //限时折扣优先或者会员价优先但是不是会员或者未开启会员价格时限时折扣优先
+ if (
+ activity.discountPriority == "limit-time" ||
+ (activity.discountPriority == "vip-price" && !isMember) ||
+ (activity.discountPriority == "vip-price" && isMember && !goods.memberPrice)
+ ) {
+ //限时折扣优先
+ return truncateToTwoDecimals(
+ new BigNumber(goods.salePrice)
+ .times(activity.discountRate / 100)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber()
+ );
+ }
+ if (activity.discountPriority == "vip-price" && isMember) {
+ return memberPrice.toNumber();
+ }
+
+ // 处理活动与会员的同享/不同享逻辑
+ if (activity.vipPriceShare) {
+ // 同享:会员价基础上叠加工活动折扣
+ return truncateToTwoDecimals(
+ memberPrice.multipliedBy(activity.discountRate).toNumber()
+ );
+ } else {
+ // 不同享:取会员价和活动价的最小值(活动价用SKU原价计算)
+ const basePriceForActivity = new BigNumber(
+ goods.skuData?.salePrice ?? goods.salePrice
+ );
+ const activityPrice = basePriceForActivity.multipliedBy(
+ activity.discountRate
+ );
+
+ return truncateToTwoDecimals(
+ memberPrice.isLessThanOrEqualTo(activityPrice)
+ ? memberPrice.toNumber()
+ : activityPrice.toNumber()
+ );
+ }
+}
+
+/**
+ * 计算商品原价总和(所有商品:原价*数量,含临时菜、赠菜,用SKU原价优先)
+ * @param goodsList 商品列表
+ * @returns 商品原价总和(元)
+ */
+export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number {
+ let total = new BigNumber(0);
+
+ for (const goods of goodsList) {
+ const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+ let basePrice = new BigNumber(0);
+ if (goods.is_temporary) {
+ basePrice = new BigNumber(goods?.discountSaleAmount ?? 0);
+ } else if (goods.is_gift) {
+ basePrice = new BigNumber(0);
+ } else {
+ basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); // SKU原价优先
+ }
+
+ total = total.plus(basePrice.multipliedBy(availableNum));
+ }
+
+ return truncateToTwoDecimals(total.toNumber());
+}
+
+/**
+ * 计算商品实际总价(所有商品:实际单价*数量,含临时菜、赠菜,按商品ID匹配活动)
+ * @param goodsList 商品列表
+ * @param config 订单额外配置(含会员、活动信息)
+ * @param activities 全局营销活动列表(如限时折扣,applicableProductIds为商品ID)
+ * @returns 商品实际总价(元)
+ */
+export function calcGoodsRealAmount(
+ goodsList: BaseCartItem[],
+ config: Pick<
+ OrderExtraConfig,
+ "isMember" | "memberDiscountRate" | "limitTimeDiscount"
+ >
+): number {
+ let total = new BigNumber(0);
+
+ for (const goods of goodsList) {
+ const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+ if (availableNum <= 0) continue;
+ console.log("goods", goods);
+ const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, config));
+ total = total.plus(realPrice.multipliedBy(availableNum));
+ }
+
+ return truncateToTwoDecimals(total.toNumber());
+}
+
+/**
+ * 计算商品折扣总金额(商品原价总和 - 商品实际总价)
+ * @param goodsOriginalAmount 商品原价总和
+ * @param goodsRealAmount 商品实际总价
+ * @returns 商品折扣总金额(元,≥0)
+ */
+export function calcGoodsDiscountAmount(
+ goodsOriginalAmount: number,
+ goodsRealAmount: number
+): number {
+ const original = new BigNumber(goodsOriginalAmount);
+ const real = new BigNumber(goodsRealAmount);
+ const discount = original.minus(real);
+
+ return truncateToTwoDecimals(Math.max(0, discount.toNumber()));
+}
+/**
+ * 从满减活动的多门槛中,选择最优阈值(满金额最小且减免金额最大)
+ * @param thresholds 满减阈值列表(多门槛)
+ * @param baseAmount 满减计算基数(新客立减后的金额)
+ * @param goodsOriginalAmount 商品原价总和(discountShare=0时用)
+ * @param goodsRealAmount 商品折扣后总和(discountShare=1时用)
+ * @param discountShare 与限时折扣同享:0=否,1=是
+ * @returns 最优阈值(无符合条件则返回undefined)
+ */
+export function selectOptimalThreshold(
+ thresholds: FullReductionThreshold[] = [],
+ baseAmount: number,
+ goodsOriginalAmount: number,
+ goodsRealAmount: number,
+ discountShare: number = 0
+): FullReductionThreshold | undefined {
+ if (!thresholds.length) return undefined;
+
+ // 第一步:确定满减门槛基数(根据discountShare规则)
+ const thresholdBase = baseAmount;
+
+ // 第二步:筛选「满金额≤基数」且「减免金额>0」的有效阈值
+ const validThresholds = thresholds.filter((threshold) => {
+ const fullAmount = new BigNumber(threshold.fullAmount || 0);
+ const discountAmount = new BigNumber(threshold.discountAmount || 0);
+ console.log("fullAmount", fullAmount);
+ console.log("discountAmount", discountAmount);
+
+ return (
+ fullAmount.isLessThanOrEqualTo(thresholdBase) &&
+ discountAmount.isGreaterThan(0)
+ );
+ });
+ console.log("validThresholds", validThresholds);
+
+ if (!validThresholds.length) return undefined;
+
+ // 找到抵扣金额最大的门槛项
+ const maxDiscountThreshold = validThresholds.reduce(
+ (maxItem, currentItem) => {
+ // 处理空值,默认抵扣金额为0
+ const maxDiscount = new BigNumber(maxItem?.discountAmount || 0);
+ const currentDiscount = new BigNumber(currentItem?.discountAmount || 0);
+
+ // 比较当前项和已存最大项的抵扣金额,保留更大的
+ return currentDiscount.gt(maxDiscount) ? currentItem : maxItem;
+ },
+ validThresholds[0] || null
+ ); // 初始值为数组第一项(若数组为空则返回null)
+ console.log("maxDiscountThreshold", maxDiscountThreshold);
+ return maxDiscountThreshold;
+}
+
+/**
+ * 计算满减实际减免金额(适配多门槛、同享规则)
+ * @param optimalActivity 最优满减活动
+ * @param optimalThreshold 最优满减阈值
+ * @param baseAmount 计算基数(新客立减后的金额)
+ * @returns 实际减免金额(元,未达门槛则0)
+ */
+export function calcFullReductionAmount(
+ baseAmount: number,
+ optimalActivity?: FullReductionActivity,
+ optimalThreshold?: FullReductionThreshold
+): number {
+ if (!optimalActivity || !optimalThreshold) return 0;
+
+ const baseAmountBn = new BigNumber(baseAmount);
+ const discountAmountBn = new BigNumber(optimalThreshold.discountAmount || 0);
+
+ // 1. 基数必须为正(避免减免后为负)
+ if (baseAmountBn.isLessThanOrEqualTo(0)) return 0;
+
+ // 2. 减免金额不能超过基数(避免减成负数)
+ const maxReducible = baseAmountBn;
+ const actualReduction = discountAmountBn.isLessThanOrEqualTo(maxReducible)
+ ? discountAmountBn
+ : maxReducible;
+
+ return truncateToTwoDecimals(actualReduction.toNumber());
+}
+
+// ------------------------------ 策略辅助函数 ------------------------------
+/**
+ * 根据优惠券useShops列表判断门店是否匹配(适配BaseCoupon的useShops字段)
+ */
+function isStoreMatchByList(
+ useShops: string[],
+ currentStoreId: string
+): boolean {
+ // 适用门店为空数组 → 无限制(所有门店适用)
+ if (useShops.length === 0) return true;
+ // 匹配当前门店ID(字符串比较,避免类型问题)
+ return useShops.includes(currentStoreId);
+}
+
+/**
+ * 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除,新增细分统计)
+ * @param backendCoupons 后端优惠券列表
+ * @param goodsList 商品列表
+ * @param config 订单配置(含就餐类型)
+ * @returns 最优优惠券的抵扣结果(含商品券/满减券细分)
+ */
+export function calcCouponDeduction(
+ backendCoupons: BackendCoupon[],
+ goodsList: BaseCartItem[],
+ config: Pick<
+ OrderExtraConfig,
+ "currentStoreId" | "isMember" | "memberDiscountRate"
+ > & {
+ activities: ActivityConfig[];
+ cartOrder: Record;
+ dinnerType: "dine-in" | "take-out";
+ currentTime?: Date;
+ }
+): {
+ deductionAmount: number;
+ productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券/折扣券/买一送一等)
+ fullCouponDeduction: number; // 新增:满减优惠券抵扣
+ usedCoupon?: Coupon;
+ excludedProductIds: string[]; // 排除的商品ID列表(商品ID)
+} {
+ const goodsCoupon = backendCoupons.filter((v) => v.type == 2);
+ const discountCoupon = backendCoupons.filter((v) => v.type != 2);
+
+ // 3. 计算非兑换券最优抵扣(传递已抵扣商品ID,避免重复,统计细分字段)
+ let nonExchangeResult: CouponResult = {
+ deductionAmount: discountCoupon.reduce((prve, cur): number => {
+ return prve + (cur.discountAmount || 0);
+ }, 0),
+ excludedProductIds: [],
+ usedCoupon: undefined,
+ productCouponDeduction: 0,
+ fullCouponDeduction: 0,
+ };
+
+ // 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID,统计商品券细分)
+ let exchangeResult: ExchangeCalculationResult = {
+ deductionAmount: goodsCoupon.reduce((prve, cur): number => {
+ return prve + (cur.discountAmount || 0);
+ }, 0),
+ excludedProductIds: [],
+ productCouponDeduction: 0,
+ };
+
+ // 5. 汇总结果:兑换券与非兑换券不可同时使用,取抵扣金额大的
+ const exchangeBn = new BigNumber(exchangeResult.deductionAmount);
+ const nonExchangeBn = new BigNumber(nonExchangeResult.deductionAmount);
+ const isExchangeBetter = exchangeBn.isGreaterThan(nonExchangeBn);
+
+ return {
+ deductionAmount: exchangeBn.plus(nonExchangeBn).toNumber(),
+ productCouponDeduction: exchangeResult.deductionAmount,
+ fullCouponDeduction: nonExchangeResult.deductionAmount, // 兑换券与满减券互斥,满减券抵扣置0
+ usedCoupon: isExchangeBetter ? undefined : nonExchangeResult.usedCoupon,
+ excludedProductIds: isExchangeBetter
+ ? exchangeResult.excludedProductIds
+ : nonExchangeResult.excludedProductIds,
+ };
+}
+
+// ============================ 5. 其他费用计算(无ID依赖,逻辑不变) ============================
+/**
+ * 计算总打包费(赠菜也计算,称重商品打包数量≤1)
+ * @param goodsList 商品列表
+ * @param dinnerType 就餐类型(堂食dine-in/外卖take-out)
+ * @returns 总打包费(元)
+ */
+export function calcTotalPackFee(
+ goodsList: BaseCartItem[],
+ dinnerType: "dine-in" | "take-out"
+): number {
+ // if (dinnerType !== "take-out") return 0;
+ let total = new BigNumber(0);
+
+ for (const goods of goodsList) {
+ const packNumber = goods.packNumber ? goods.packNumber * 1 : 0;
+ let availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+
+ if (availableNum === 0) continue;
+
+ // 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1)
+ let packNum = Math.min(availableNum, packNumber);
+ if (dinnerType === "take-out") {
+ packNum = availableNum;
+ }
+ if (goods.product_type === GoodsType.WEIGHT) {
+ packNum = Math.min(packNum, 1);
+ }
+
+ total = total.plus(new BigNumber(goods.packFee || 0).multipliedBy(packNum));
+ }
+
+ return truncateToTwoDecimals(total.toNumber());
+}
+
+/**
+ * 计算餐位费(按人数,不参与营销活动)
+ * @param config 餐位费配置
+ * @returns 餐位费(元,未启用则0)
+ */
+export function calcSeatFee(config: SeatFeeConfig): number {
+ if (!config.isEnabled || config.personCount == 0) return 0;
+ const personCount = Math.max(1, config.personCount); // 至少1人
+ return truncateToTwoDecimals(
+ new BigNumber(config.pricePerPerson).multipliedBy(personCount).toNumber()
+ );
+}
+
+/**
+ * 计算积分抵扣金额(按X积分=1元,不超过最大抵扣和用户积分)
+ * @param userPoints 用户当前积分
+ * @param rule 积分抵扣规则
+ * @param maxDeductionLimit 最大抵扣上限(通常为订单金额,避免超扣)
+ * @returns 积分抵扣金额 + 实际使用积分
+ */
+export function calcPointDeduction(
+ userPoints: number,
+ rule: PointDeductionRule,
+ maxDeductionLimit: number
+): {
+ deductionAmount: number;
+ usedPoints: number;
+} {
+ console.log("calcPointDeduction", userPoints, rule, maxDeductionLimit);
+ if (rule.pointsPerYuan <= 0 || userPoints <= 0) {
+ return { deductionAmount: 0, usedPoints: 0 };
+ }
+
+ const userPointsBn = new BigNumber(userPoints);
+ const pointsPerYuanBn = new BigNumber(rule.pointsPerYuan);
+ const maxLimitBn = new BigNumber(maxDeductionLimit);
+
+ // 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限)
+ const maxDeductByPoints = userPointsBn.dividedBy(pointsPerYuanBn);
+ const maxDeductAmount = maxDeductByPoints.isLessThan(
+ rule.maxDeductionAmount ?? Infinity
+ )
+ ? maxDeductByPoints
+ : new BigNumber(rule.maxDeductionAmount || Infinity).isLessThan(maxLimitBn)
+ ? maxDeductByPoints
+ : maxLimitBn;
+
+ // 实际使用积分 = 抵扣金额 * 积分兑换比例
+ const usedPoints = maxDeductAmount.multipliedBy(pointsPerYuanBn);
+
+ return {
+ deductionAmount: truncateToTwoDecimals(maxDeductAmount.toNumber()),
+ usedPoints: truncateToTwoDecimals(
+ Math.min(usedPoints.toNumber(), userPoints)
+ ), // 避免积分超扣
+ };
+}
+
+function calcVipDiscountAmount(
+ goodsRealAmount: number,
+ shopUserInfo: ShopUserInfo
+): number {
+ if (!shopUserInfo.isVip || shopUserInfo.discount === 0) return 0;
+ if (shopUserInfo.isVip == 1 && shopUserInfo.isMemberPrice != 1) {
+ return 0;
+ }
+ console.log("goodsRealAmount", goodsRealAmount);
+ console.log("discount", (100 - (shopUserInfo.discount || 0)) / 100);
+ return truncateToTwoDecimals(
+ new BigNumber(goodsRealAmount)
+ .times((100 - (shopUserInfo.discount || 0)) / 100)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber()
+ );
+}
+
+// ============================ 6. 订单总费用汇总与实付金额计算(核心入口,补充细分字段) ============================
+/**
+ * 计算订单所有费用子项并汇总(核心入口函数)
+ * @param goodsList 购物车商品列表
+ * @param dinnerType 就餐类型
+ * @param backendCoupons 后端优惠券列表
+ * @param activities 全局营销活动列表
+ * @param config 订单额外配置(会员、积分、餐位费等)
+ * @param cartOrder 商品加入购物车顺序(key=购物车ID,value=时间戳)
+ * @param currentTime 当前时间(用于优惠券有效期判断)
+ * @returns 订单费用汇总(含所有子项和实付金额,新增商品券/满减券细分)
+ */
+export function calculateOrderCostSummary(
+ goodsList: BaseCartItem[],
+ dinnerType: "dine-in" | "take-out", // 前端就餐类型
+ backendCoupons: BackendCoupon[] = [],
+ activities: ActivityConfig[] = [],
+ config: OrderExtraConfig, // 含后端满减活动、currentDinnerType
+ cartOrder: Record = {},
+ currentTime: Date = new Date()
+): OrderCostSummary {
+ //是否使用霸王餐,霸王餐配置
+ const {
+ isFreeDine,
+ freeDineConfig,
+ limitTimeDiscount,
+ fullReductionActivities,
+ shopUserInfo,
+ } = config;
+ console.log("shopUserInfo", shopUserInfo);
+
+ // ------------------------------ 1. 基础费用计算 ------------------------------
+ const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); // 商品原价总和
+ const goodsRealAmount = calcGoodsRealAmount(
+ // 商品折扣后总和
+ goodsList,
+ {
+ isMember: config.isMember,
+ memberDiscountRate: config.memberDiscountRate,
+ limitTimeDiscount: config.limitTimeDiscount,
+ }
+ );
+ const goodsDiscountAmount = calcGoodsDiscountAmount(
+ goodsOriginalAmount,
+ goodsRealAmount
+ ); // 商品折扣金额
+
+ const newUserDiscount = config.newUserDiscount || 0; // 新客立减
+
+ // 其他费用计算(原有逻辑不变) ------------------------------
+ const packFee = calcTotalPackFee(goodsList, dinnerType); // 打包费
+ let seatFee = calcSeatFee(config.seatFeeConfig); // 餐位费
+ seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费
+ const additionalFee = Math.max(0, config.additionalFee); // 附加费
+
+ // ------------------------------ 2. 满减活动计算(核心步骤) ------------------------------
+ let usedFullReductionActivity: FullReductionActivity | undefined;
+ let usedFullReductionThreshold: FullReductionThreshold | undefined;
+ let fullReductionAmount = 0;
+ let usedFullReductionActivityFullAmount = calcFullReductionActivityFullAmount(
+ goodsList,
+ usedFullReductionActivity,
+ config.limitTimeDiscount,
+ config.isMember,
+ seatFee,
+ packFee
+ );
+
+ // 2.1 筛选最优满减活动(后端活动列表、当前店铺、就餐类型、时间)
+ usedFullReductionActivity = filterOptimalFullReductionActivity(
+ config.fullReductionActivities,
+ Number(config.currentStoreId), // 转换为数字(后端shopId是number)
+ config.currentDinnerType, // 后端useType匹配的就餐类型(如"dine")
+ currentTime
+ );
+
+ // 2.2 计算满减基数(先扣新客立减)
+ let baseAfterNewUserDiscount = new BigNumber(
+ limitTimeDiscount &&
+ limitTimeDiscount.id &&
+ usedFullReductionActivity &&
+ !usedFullReductionActivity.discountShare
+ ? goodsRealAmount
+ : goodsRealAmount
+ )
+ .minus(newUserDiscount)
+ .plus(packFee)
+ .plus(seatFee)
+ .plus(additionalFee)
+ .toNumber();
+ baseAfterNewUserDiscount =
+ baseAfterNewUserDiscount > 0 ? baseAfterNewUserDiscount : 0;
+
+ // 2.3 选择最优满减阈值(多门槛场景)
+ if (usedFullReductionActivity) {
+ //计算当前满减活动的门槛金额
+ usedFullReductionActivityFullAmount = calcFullReductionActivityFullAmount(
+ goodsList,
+ usedFullReductionActivity,
+ config.limitTimeDiscount,
+ config.isMember,
+ seatFee,
+ packFee
+ );
+
+ usedFullReductionThreshold = selectOptimalThreshold(
+ usedFullReductionActivity.thresholds,
+ usedFullReductionActivityFullAmount,
+ goodsOriginalAmount,
+ goodsRealAmount,
+ usedFullReductionActivity.discountShare || 0 // 与限时折扣同享规则
+ );
+
+ // 2.4 计算满减实际减免金额
+ fullReductionAmount = calcFullReductionAmount(
+ baseAfterNewUserDiscount,
+ usedFullReductionActivity,
+ usedFullReductionThreshold
+ );
+ }
+
+ // ------------------------------ 3. 优惠券抵扣(适配满减同享规则) ------------------------------
+ let couponDeductionAmount = 0;
+ let productCouponDeduction = 0;
+ let fullCouponDeduction = 0;
+ let usedCoupon: Coupon | undefined;
+ let excludedProductIds: string[] = [];
+
+ const couponResult = calcCouponDeduction(
+ // 原有优惠券计算函数
+ backendCoupons,
+ goodsList,
+ {
+ currentStoreId: config.currentStoreId,
+ isMember: config.isMember,
+ memberDiscountRate: config.memberDiscountRate,
+ activities,
+ cartOrder,
+ dinnerType,
+ currentTime,
+ }
+ );
+ console.log("couponResult", couponResult);
+ couponDeductionAmount = couponResult.deductionAmount;
+ productCouponDeduction = couponResult.productCouponDeduction;
+ fullCouponDeduction = couponResult.fullCouponDeduction;
+ usedCoupon = couponResult.usedCoupon;
+ excludedProductIds = couponResult.excludedProductIds;
+
+ // 若满减与优惠券同享(couponShare=1),才计算优惠券;否则优惠券抵扣为0
+ if (
+ usedFullReductionThreshold &&
+ (!usedFullReductionActivity || !usedFullReductionActivity.couponShare)
+ ) {
+ couponDeductionAmount = 0;
+ productCouponDeduction = 0;
+ fullCouponDeduction = 0;
+ usedCoupon = undefined;
+ excludedProductIds = [];
+ }
+
+ // ------------------------------ 4. 积分抵扣(适配满减同享规则) ------------------------------
+ let pointDeductionAmount = 0;
+ let usedPoints = 0;
+
+ // 计算积分抵扣基数(商品折扣后 - 新客立减 - 满减 - 优惠券 + 餐位费 + 打包费 + 附加费)
+ let maxPointDeductionLimit = new BigNumber(goodsRealAmount)
+ .minus(newUserDiscount)
+ .minus(fullReductionAmount)
+ .minus(couponDeductionAmount)
+ .plus(seatFee)
+ .plus(packFee)
+ .plus(additionalFee)
+ .toNumber();
+ maxPointDeductionLimit =
+ maxPointDeductionLimit > 0 ? maxPointDeductionLimit : 0;
+
+ const pointResult = calcPointDeduction(
+ config.userPoints,
+ config.pointDeductionRule,
+ maxPointDeductionLimit
+ );
+
+ pointDeductionAmount = pointResult.deductionAmount;
+ usedPoints = pointResult.usedPoints;
+ // 若满减与积分不同享(pointsShare=1)积分抵扣为0
+ if (
+ usedFullReductionThreshold &&
+ (!usedFullReductionActivity || !usedFullReductionActivity.pointsShare)
+ ) {
+ pointDeductionAmount = 0;
+ usedPoints = 0;
+ }
+
+ //使用霸王餐
+ if (isFreeDine && freeDineConfig && freeDineConfig.enable) {
+ fullReductionAmount = 0;
+ //不与优惠券同享
+ if (!freeDineConfig.withCoupon) {
+ couponDeductionAmount = 0;
+ productCouponDeduction = 0;
+ fullCouponDeduction = 0;
+ usedCoupon = undefined;
+ excludedProductIds = [];
+ }
+ //不与积分同享
+ if (!freeDineConfig.withPoints) {
+ pointDeductionAmount = 0;
+ usedPoints = 0;
+ }
+ }
+
+ // 商家减免计算(原有逻辑不变)
+ const merchantReductionConfig = config.merchantReduction;
+ let merchantReductionActualAmount = 0;
+ const maxMerchantReductionLimit = new BigNumber(goodsRealAmount)
+ .minus(newUserDiscount)
+ .minus(fullReductionAmount)
+ .minus(couponDeductionAmount)
+ .minus(pointDeductionAmount)
+ .plus(seatFee)
+ .plus(packFee)
+ .isGreaterThan(0)
+ ? new BigNumber(goodsRealAmount)
+ .minus(newUserDiscount)
+ .minus(fullReductionAmount)
+ .minus(couponDeductionAmount)
+ .minus(pointDeductionAmount)
+ .plus(seatFee)
+ .plus(packFee)
+ .toNumber()
+ : 0;
+
+ switch (merchantReductionConfig.type) {
+ case MerchantReductionType.FIXED_AMOUNT:
+ merchantReductionActualAmount = Math.min(
+ merchantReductionConfig.fixedAmount || 0,
+ maxMerchantReductionLimit
+ );
+ break;
+ case MerchantReductionType.DISCOUNT_RATE:
+ const validRate =
+ Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0)) /
+ 100;
+ merchantReductionActualAmount =
+ maxMerchantReductionLimit * (1 - validRate);
+ break;
+ }
+ merchantReductionActualAmount = Math.max(
+ 0,
+ truncateToTwoDecimals(merchantReductionActualAmount)
+ );
+
+ // 会员折扣减免
+ const vipDiscountAmount = calcVipDiscountAmount(
+ new BigNumber(goodsRealAmount)
+ .minus(couponDeductionAmount)
+ .plus(packFee)
+ .plus(seatFee)
+ .minus(newUserDiscount)
+ .minus(fullReductionAmount)
+ .toNumber(),
+ shopUserInfo
+ ); // 会员折扣减免金额
+ // ------------------------------ 6. 最终实付金额计算 ------------------------------
+ const finalPayAmountBn = new BigNumber(goodsRealAmount)
+ .minus(newUserDiscount)
+ .minus(vipDiscountAmount)
+ .minus(fullReductionAmount)
+ .minus(couponDeductionAmount)
+ .minus(pointDeductionAmount)
+ .minus(merchantReductionActualAmount)
+ .plus(seatFee)
+ .plus(packFee)
+ .plus(additionalFee);
+ let finalPayAmount = Math.max(
+ 0,
+ truncateToTwoDecimals(finalPayAmountBn.toNumber())
+ );
+ // ------------------------------ 使用霸王餐计算 ------------------------------
+ let orderOriginFinalPayAmount = finalPayAmount;
+ if (isFreeDine && freeDineConfig && freeDineConfig.enable) {
+ finalPayAmount = BigNumber(finalPayAmount)
+ .times(freeDineConfig.rechargeTimes)
+ .toNumber();
+ }
+
+ // ------------------------------ 7. 总优惠金额计算 ------------------------------
+ const totalDiscountAmount = truncateToTwoDecimals(
+ new BigNumber(goodsDiscountAmount)
+ .plus(newUserDiscount)
+ .plus(fullReductionAmount)
+ .plus(couponDeductionAmount)
+ .plus(pointDeductionAmount)
+ .plus(merchantReductionActualAmount)
+ .plus(vipDiscountAmount)
+ .toNumber()
+ );
+ //积分可抵扣最大金额 最终支付金额+积分抵扣-商家减免
+ const scoreMaxMoney = new BigNumber(finalPayAmount)
+ .plus(pointDeductionAmount)
+ .minus(merchantReductionActualAmount)
+ .toNumber();
+
+ // ------------------------------ 8. 返回完整结果 ------------------------------
+ return {
+ goodsTotal: goodsList.reduce(
+ (sum, g) => sum + Math.max(0, g.number - (g.returnNum || 0)),
+ 0
+ ),
+ goodsRealAmount,
+ goodsOriginalAmount,
+ goodsDiscountAmount,
+ couponDeductionAmount,
+ productCouponDeduction,
+ fullCouponDeduction,
+ pointDeductionAmount,
+ seatFee,
+ packFee,
+ totalDiscountAmount,
+ //最终支付原金额
+ orderOriginFinalPayAmount,
+ //积分最大可抵扣金额
+ scoreMaxMoney,
+ // 满减活动明细(后端字段)
+ fullReduction: {
+ usedFullReductionActivityFullAmount: usedFullReductionActivityFullAmount,
+ usedActivity: usedFullReductionActivity,
+ usedThreshold: usedFullReductionThreshold,
+ actualAmount: truncateToTwoDecimals(fullReductionAmount),
+ },
+ vipDiscountAmount: vipDiscountAmount, //会员折扣减免金额
+ merchantReduction: {
+ type: merchantReductionConfig.type,
+ originalConfig: merchantReductionConfig,
+ actualAmount: merchantReductionActualAmount,
+ },
+ additionalFee,
+ finalPayAmount,
+ couponUsed: usedCoupon,
+ pointUsed: usedPoints,
+ newUserDiscount,
+ dinnerType,
+ config,
+ };
+}
+
+export function isWeightGoods(goods: BaseCartItem): boolean {
+ return goods.product_type === GoodsType.WEIGHT;
+}
+
+// ============================ 7. 对外暴露工具库 ============================
+export const OrderPriceCalculator = {
+ // 基础工具
+ truncateToTwoDecimals,
+ isTemporaryGoods,
+ isGiftGoods,
+ formatDiscountRate,
+ filterThresholdGoods,
+ isWeightGoods,
+ // 商品价格计算
+ calcSingleGoodsRealPrice,
+ calcGoodsOriginalAmount,
+ calcGoodsRealAmount,
+ calcGoodsDiscountAmount,
+ //满减活动工具
+ filterOptimalFullReductionActivity,
+ // 优惠券计算
+ calcCouponDeduction,
+ // 其他费用计算
+ calcTotalPackFee,
+ calcSeatFee,
+ calcPointDeduction,
+ // 核心入口
+ calculateOrderCostSummary,
+ // 枚举导出
+ Enums: {
+ GoodsType,
+ CouponType,
+ ActivityType,
+ WEEKDAY_MAP,
+ },
+};
+
+export default OrderPriceCalculator;
diff --git a/utils/goods-utils - 副本.js b/utils/goods-utils - 副本.js
index db21553..0012e45 100644
--- a/utils/goods-utils - 副本.js
+++ b/utils/goods-utils - 副本.js
@@ -1,27 +1,55 @@
-import {
- BigNumber
-} from "bignumber.js";
+import { BigNumber } from "bignumber.js";
import _ from "lodash";
-
-
/**
* 返回商品单价
* @param goods 商品
* @param user 用户信息
- * @param {Object} shopInfo
+ * @param {Object} shopInfo 店铺信息
+ * @param {boolean} limitTimeDiscount - 限时折扣
*/
-export function returnGoodsPrice(goods, user, shopInfo) {
- if (goods.discount_sale_amount * 1 > 0) {
- return goods.discount_sale_amount;
- }
- if(shopInfo&&!shopInfo.isMemberPrice){
- return goods.salePrice;
- }
- if (user.isVip && goods.memberPrice * 1 <= goods.salePrice * 1 && goods.memberPrice * 1 > 0) {
- return goods.memberPrice;
- }
- return goods.salePrice;
+export function returnGoodsPrice(goods, user, shopInfo, limitTimeDiscount) {
+ if (!goods) {
+ return 0;
+ }
+ // 商家改价
+ if (goods.discount_sale_amount * 1 > 0) {
+ return goods.discount_sale_amount;
+ }
+ // 限时折扣
+ if (limitTimeDiscount && limitTimeDiscount.id) {
+ if (
+ limitTimeDiscount.foodType == 1 &&
+ limitTimeDiscount.discountPriority == "limit-time"
+ ) {
+ return new BigNumber(goods.salePrice)
+ .times(limitTimeDiscount.discountRate / 100)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ }
+ const canUseFoods = limitTimeDiscount.foods.split(",");
+ if (
+ limitTimeDiscount.foodType == 2 &&
+ limitTimeDiscount.discountPriority == "limit-time" &&
+ canUseFoods.includes(`${goods.productId}`)
+ ) {
+ return new BigNumber(goods.salePrice)
+ .times(limitTimeDiscount.discountRate / 100)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ }
+ }
+ if (shopInfo && !shopInfo.isMemberPrice) {
+ return goods.salePrice;
+ }
+ if (
+ user.isVip &&
+ goods.memberPrice * 1 <= goods.salePrice * 1 &&
+ goods.memberPrice * 1 > 0
+ ) {
+ return goods.memberPrice;
+ }
+ return goods.salePrice;
}
/**
@@ -29,15 +57,15 @@ export function returnGoodsPrice(goods, user, shopInfo) {
* @param arr 商品列表
*/
export function returnGoodsGroupMap(arr) {
- let map = {};
- arr.forEach((v) => {
- const key = v.productId + "_" + v.skuId;
- if (!map[key]) {
- map[key] = [];
- }
- map[key].push(v);
- });
- return map;
+ let map = {};
+ arr.forEach((v) => {
+ const key = v.productId + "_" + v.skuId;
+ if (!map[key]) {
+ map[key] = [];
+ }
+ map[key].push(v);
+ });
+ return map;
}
/**
@@ -45,17 +73,17 @@ export function returnGoodsGroupMap(arr) {
* @param coupon
*/
export function returnCoupType(coupon) {
- const couponTypes = {
- 1: "满减券",
- 2: "商品券",
- 3: "折扣券",
- 4: "第二件半价券",
- 5: "消费送券",
- 6: "买一送一券",
- 7: "固定价格券",
- 8: "免配送费券",
- };
- return couponTypes[coupon.type] || "未知类型";
+ const couponTypes = {
+ 1: "满减券",
+ 2: "商品券",
+ 3: "折扣券",
+ 4: "第二件半价券",
+ 5: "消费送券",
+ 6: "买一送一券",
+ 7: "固定价格券",
+ 8: "免配送费券",
+ };
+ return couponTypes[coupon.type] || "未知类型";
}
/**
@@ -65,22 +93,37 @@ export function returnCoupType(coupon) {
* @param user 用户信息
*/
export function returnCanDikouGoodsArr(canDikouGoodsArr, selCoupon, user) {
- const goodsCouponGoods = selCoupon
- .filter((v) => v.type == 2)
- .reduce((prve, cur) => {
- prve.push(...cur.discount.hasDiscountGoodsArr);
- return prve;
- }, []);
- const arr = _.cloneDeep(canDikouGoodsArr)
- .map((v) => {
- const findCart = goodsCouponGoods.find((carts) => carts.id == v.id);
- if (findCart) {
- v.num -= findCart.num;
- }
- return v;
- })
- .filter((v) => v.num > 0);
- return arr;
+ const types = [2, 4, 6];
+ // 收集已抵扣商品并关联对应的优惠券类型
+ const goodsCouponGoods = selCoupon
+ .filter((v) => types.includes(v.type))
+ .reduce((prev, cur) => {
+ // 给每个抵扣商品添加所属优惠券类型
+ const goodsWithType = cur.discount.hasDiscountGoodsArr.map((goods) => ({
+ ...goods,
+ couponType: cur.type, // 记录该商品是被哪种类型的优惠券抵扣的
+ }));
+ prev.push(...goodsWithType);
+ return prev;
+ }, []);
+ const arr = _.cloneDeep(canDikouGoodsArr)
+ .map((v) => {
+ const findCart = goodsCouponGoods.find((carts) => carts.id == v.id);
+ if (findCart) {
+ // 根据优惠券类型判断扣减数量
+ if ([4, 6].includes(findCart.couponType)) {
+ // 类型4(第二件半价)或6(买一送一),数量减2
+ v.num -= 2;
+ } else {
+ // 其他类型(如类型2商品券),按原逻辑扣减对应数量
+ v.num -= findCart.num;
+ }
+ }
+ return v;
+ })
+ .filter((v) => v.num > 0); // 过滤掉数量<=0的商品
+
+ return arr;
}
/**
@@ -97,139 +140,208 @@ export function returnCanDikouGoodsArr(canDikouGoodsArr, selCoupon, user) {
* @param {Object} args.user - 用户信息对象
* @param {Object} args.selCoupon - 已经选择的优惠券信息对象
* @param {Object} args.shopInfo
+ * @param {boolean} args.limitTimeDiscount - 限时折扣
* @returns {Object} - { canUse: boolean, reason: string } 可用状态及不可用原因
*/
export function returnCouponCanUse(args) {
- let {
- canDikouGoodsArr,
- coupon,
- goodsOrderPrice,
- user,
- selCoupon,
- shopInfo
- } = args;
+ let {
+ canDikouGoodsArr,
+ coupon,
+ goodsOrderPrice,
+ user,
+ selCoupon,
+ shopInfo,
+ isMemberPrice,
+ limitTimeDiscount,
+ } = args;
+ // 优惠券未启用
+ if (!coupon.use) {
+ return {
+ canUse: false,
+ reason: coupon.noUseRestrictions || "不在可用时间段内",
+ };
+ }
+ if(limitTimeDiscount&&limitTimeDiscount.id&&!coupon.discountShare){
+ return {
+ canUse: false,
+ reason: coupon.noUseRestrictions || "不可与限时折扣同享",
+ };
+ }
- // 优惠券未启用
- if (!coupon.use) {
- return {
- canUse: false,
- reason: "优惠券未启用"
- };
- }
+ // 计算门槛金额
+ let fullAmount = goodsOrderPrice;
+ canDikouGoodsArr = returnCanDikouGoodsArr(
+ canDikouGoodsArr,
+ selCoupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
+ //优惠券指定门槛商品列表
+ let canCalcGoodsArr = [...canDikouGoodsArr];
+ //部分商品参与门槛计算
+ if (coupon.thresholdFoods.length) {
+ canCalcGoodsArr = canDikouGoodsArr.filter((v) => {
+ return coupon.thresholdFoods.find((food) => food.id == v.productId);
+ });
+ fullAmount = canCalcGoodsArr.reduce((pre, cur) => {
+ return (
+ pre + returnGoodsPrice(cur, user, shopInfo, limitTimeDiscount) * cur.num
+ );
+ }, 0);
+ }
- canDikouGoodsArr = returnCanDikouGoodsArr(canDikouGoodsArr, selCoupon, user,shopInfo);
- // 计算门槛金额
- let fullAmount = goodsOrderPrice;
- // 是否抵扣全部商品
- const isDikouAll = coupon.useFoods.length === 0;
- let canCalcGoodsArr = [];
- // 订单里参与门槛计算的商品
- if (!isDikouAll) {
- canCalcGoodsArr = canDikouGoodsArr.filter((v) => {
- return coupon.useFoods.find((food) => food.id == v.productId);
- });
- fullAmount = canCalcGoodsArr.reduce((pre, cur) => {
- return pre + returnGoodsPrice(cur, user,shopInfo) * cur.num;
- }, 0);
- }
+ // 是否全部商品可用
+ const isDikouAll = coupon.useFoods.length === 0;
+ // 订单可用商品列表
+ let canUseGoodsArr = [];
+ if (!isDikouAll) {
+ canUseGoodsArr = canDikouGoodsArr.filter((v) => {
+ return coupon.useFoods.find((food) => food.id == v.productId);
+ });
+ }
+ if (user.isVip && !coupon.vipPriceShare) {
+ return {
+ canUse: false,
+ reason: "非会员可用",
+ };
+ }
+ if (selCoupon.length > 0 && !selCoupon[0].otherCouponShare) {
+ return {
+ canUse: false,
+ reason: "当前选中的券不可与其他券同享",
+ };
+ }
+ if (selCoupon.length > 0 && !coupon.otherCouponShare) {
+ return {
+ canUse: false,
+ reason: "当前选中的券不可与其他券同享",
+ };
+ }
+ // 满减券和折扣券计算门槛金额是否满足
+ if ([1, 3].includes(coupon.type)) {
+ if (canCalcGoodsArr.length <= 0) {
+ return {
+ canUse: false,
+ reason: "没有可参与计算门槛的商品",
+ };
+ }
+ // 不满足门槛金额
+ if (fullAmount < coupon.fullAmount) {
+ return {
+ canUse: false,
+ reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`,
+ };
+ }
+ }
+ // 商品兑换券,第二件半价和买一送一判断是否有可用商品
+ if ([2, 4, 5].includes(coupon.type)) {
+ if (coupon.type == 2 && fullAmount < coupon.fullAmount) {
+ return {
+ canUse: false,
+ reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`,
+ };
+ }
- // 没有符合条件的商品
- if (!isDikouAll && canCalcGoodsArr.length === 0) {
- return {
- canUse: false,
- reason: "没有符合条件的商品"
- };
- }
+ // 没有符合条件的商品
+ if (isDikouAll && canDikouGoodsArr.length === 0) {
+ return {
+ canUse: false,
+ reason: "没有符合条件的商品",
+ };
+ }
+ if (!isDikouAll && canUseGoodsArr.length === 0) {
+ return {
+ canUse: false,
+ reason: "没有符合条件的商品",
+ };
+ }
+ }
+ //商品兑换券是否达到门槛金额
+ if (coupon.type == 2 && goodsOrderPrice < coupon.fullAmount) {
+ return {
+ canUse: false,
+ reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`,
+ };
+ }
- // 不满足门槛金额
- if (fullAmount < coupon.fullAmount) {
- return {
- canUse: false,
- reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`
- };
- }
+ // 买一送一券特殊验证
+ if (coupon.type === 6) {
+ let canUse = false;
+ if (isDikouAll) {
+ canUse = canDikouGoodsArr.some((v) => v.num >= 2);
+ } else if (canUseGoodsArr.length > 0) {
+ canUse = canUseGoodsArr.some((v) => v.num >= 2);
+ }
- // 商品券特殊验证
- if (coupon.type === 2) {
- if (!(isDikouAll || canCalcGoodsArr.length > 0)) {
- return {
- canUse: false,
- reason: "没有符合条件的商品可抵扣"
- };
- }
- }
+ if (!canUse) {
+ return {
+ canUse: false,
+ reason: "需要购买至少2件相同的商品才能使用",
+ };
+ }
+ }
- // 买一送一券特殊验证
- if (coupon.type === 6) {
- let canUse = false;
- if (isDikouAll) {
- canUse = canDikouGoodsArr.some((v) => v.num >= 2);
- } else if (canCalcGoodsArr.length > 0) {
- canUse = canCalcGoodsArr.some((v) => v.num >= 2);
- }
+ // 第二件半价券特殊验证
+ if (coupon.type === 4) {
+ let canUse = false;
+ if (isDikouAll) {
+ canUse = canDikouGoodsArr.some((v) => v.num >= 2);
+ } else if (canUseGoodsArr.length > 0) {
+ canUse = canUseGoodsArr.some((v) => v.num >= 2);
+ }
+ if (!canUse) {
+ return {
+ canUse: false,
+ reason: "需要购买至少2件相同的商品才能使用",
+ };
+ }
+ }
- if (!canUse) {
- return {
- canUse: false,
- reason: "需要购买至少2件相同的商品才能使用"
- };
- }
- }
-
- // 第二件半价券特殊验证
- if (coupon.type === 4) {
- let canUse = false;
- if (isDikouAll) {
- canUse = canDikouGoodsArr.some((v) => v.num >= 2);
- } else if (canCalcGoodsArr.length > 0) {
- canUse = canCalcGoodsArr.some((v) => v.num >= 2);
- }
-
- if (!canUse) {
- return {
- canUse: false,
- reason: "需要购买至少2件相同的商品才能使用"
- };
- }
- }
-
- // 所有条件都满足
- return {
- canUse: true,
- reason: ""
- };
+ // 所有条件都满足
+ return {
+ canUse: true,
+ reason: "",
+ };
}
-
/**
* 计算抵扣商品金额
* @param discountGoodsArr 可抵扣商品列表
* @param discountNum 抵扣数量
* @param user 用户信息
- * @param {Object} shopInfo 店铺信息
+ * @param {Object} shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-export function calcDiscountGoodsArrPrice(discountGoodsArr, discountNum, user,shopInfo) {
- let hasCountNum = 0;
- let discountPrice = 0;
- let hasDiscountGoodsArr = [];
- for (let i = 0; i < discountGoodsArr.length; i++) {
- if (hasCountNum >= discountNum) {
- break;
- }
- const goods = discountGoodsArr[i];
- const shengyuNum = discountNum - hasCountNum;
- const num = Math.min(goods.num, shengyuNum);
- discountPrice += returnGoodsPrice(goods, user,shopInfo) * num;
- hasCountNum += num;
- hasDiscountGoodsArr.push({
- ...goods,
- num
- });
- }
- return {
- discountPrice,
- hasDiscountGoodsArr
- };
+export function calcDiscountGoodsArrPrice(
+ discountGoodsArr,
+ discountNum,
+ user,
+ shopInfo,limitTimeDiscount
+) {
+ let hasCountNum = 0;
+ let discountPrice = 0;
+ let hasDiscountGoodsArr = [];
+ for (let i = 0; i < discountGoodsArr.length; i++) {
+ if (hasCountNum >= discountNum) {
+ break;
+ }
+ const goods = discountGoodsArr[i];
+ const shengyuNum = discountNum - hasCountNum;
+ const num = Math.min(goods.num, shengyuNum);
+ discountPrice += returnGoodsPrice(goods, user, shopInfo,limitTimeDiscount) * num;
+
+ hasCountNum += num;
+ hasDiscountGoodsArr.push({
+ ...goods,
+ num,
+ });
+ }
+
+ return {
+ discountPrice,
+ hasDiscountGoodsArr,
+ };
}
/**
@@ -239,23 +351,51 @@ export function calcDiscountGoodsArrPrice(discountGoodsArr, discountNum, user,sh
* @param user 用户信息
* @param goodsOrderPrice 商品订单金额
* @param selCoupon 已选择的优惠券列表
+ * @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-export function returnCouponDiscount(arr, coupon, user, goodsOrderPrice, selCoupon) {
- console.log('arr', arr);
- const canDikouGoodsArr = returnCanDikouGoodsArr(arr, selCoupon, user);
- console.log('canDikouGoodsArr', canDikouGoodsArr);
- if (coupon.type == 2) {
- return returnCouponProductDiscount(canDikouGoodsArr, coupon, user, goodsOrderPrice);
- }
- if (coupon.type == 6) {
- return returnCouponBuyOneGiveOneDiscount(canDikouGoodsArr, coupon, user, goodsOrderPrice);
- }
- if (coupon.type == 4) {
- return returnSecoendDiscount(canDikouGoodsArr, coupon, user, goodsOrderPrice);
- }
- if (coupon.type == 3) {
- return returnCouponZhekouDiscount(canDikouGoodsArr, coupon, user, goodsOrderPrice, selCoupon);
- }
+export function returnCouponDiscount(
+ arr,
+ coupon,
+ user,
+ goodsOrderPrice,
+ selCoupon,
+ shopInfo,limitTimeDiscount
+) {
+ arr = returnCanDikouGoods(arr, user, shopInfo);
+ const canDikouGoodsArr = returnCanDikouGoodsArr(arr, selCoupon, user);
+ if (coupon.type == 2) {
+ return returnCouponProductDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
+ }
+ if (coupon.type == 6) {
+ const result = returnCouponBuyOneGiveOneDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
+ return result;
+ }
+ if (coupon.type == 4) {
+ return returnSecoendDiscount(canDikouGoodsArr, coupon, user, shopInfo,limitTimeDiscount);
+ }
+ if (coupon.type == 3) {
+ return returnCouponZhekouDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ goodsOrderPrice,
+ selCoupon,
+ limitTimeDiscount
+ );
+ }
}
/**
@@ -265,41 +405,53 @@ export function returnCouponDiscount(arr, coupon, user, goodsOrderPrice, selCoup
* @param user 用户信息
* @param goodsOrderPrice 商品订单金额
* @param selCoupon 已选择的优惠券列表
+ * @param limitTimeDiscount 限时折扣
*
*/
export function returnCouponZhekouDiscount(
- canDikouGoodsArr,
- coupon,
- user,
- goodsOrderPrice,
- selCoupon
+ canDikouGoodsArr,
+ coupon,
+ user,
+ goodsOrderPrice,
+ selCoupon,limitTimeDiscount
) {
- const {
- discountRate,
- maxDiscountAmount
- } = coupon;
+ const { discountRate, maxDiscountAmount } = coupon;
- const goodsCouponDiscount = selCoupon
- .filter((v) => v.type == 2)
- .reduce((prve, cur) => {
- return prve + cur.discount.discountPrice;
- }, 0);
- goodsOrderPrice -= goodsCouponDiscount;
- // 使用bignumber处理高精度计算
- // 1. 计算折扣率(百分比转小数):discountRate / 100
- const discountRatio = new BigNumber(discountRate).dividedBy(100);
- // 2. 计算优惠比例:1 - 折扣率(例如:8折的优惠比例是 1 - 0.8 = 0.2)
- const discountAmountRatio = new BigNumber(1).minus(discountRatio);
- // 3. 计算折扣金额:商品订单金额 × 优惠比例
- let discountPrice = new BigNumber(goodsOrderPrice).times(discountAmountRatio).toNumber();
- if (maxDiscountAmount != 0) {
- discountPrice = discountPrice >= maxDiscountAmount ? maxDiscountAmount : discountPrice;
- }
+ // 计算商品优惠券折扣总和,使用BigNumber避免精度问题
+ const goodsCouponDiscount = selCoupon
+ .filter((v) => v.type == 2)
+ .reduce((prve, cur) => {
+ return new BigNumber(prve).plus(
+ new BigNumber(cur.discount.discountPrice)
+ );
+ }, new BigNumber(0));
- return {
- discountPrice, // 折扣抵扣金额(即优惠的金额)
- hasDiscountGoodsArr: [],
- };
+ // 将商品订单价格转换为BigNumber并减去优惠券折扣
+ const adjustedGoodsOrderPrice = new BigNumber(goodsOrderPrice).minus(
+ goodsCouponDiscount
+ );
+
+ // 计算优惠比例:(100 - 折扣率) / 100
+ const discountAmountRatio = new BigNumber(100)
+ .minus(discountRate)
+ .dividedBy(100);
+
+ // 计算折扣金额:调整后的商品订单金额 × 优惠比例
+ let discountPrice = adjustedGoodsOrderPrice
+ .times(discountAmountRatio)
+ .decimalPlaces(2, BigNumber.ROUND_FLOOR)
+ .toNumber();
+
+ // 应用最大折扣金额限制
+ if (maxDiscountAmount !== 0) {
+ discountPrice =
+ discountPrice >= maxDiscountAmount ? maxDiscountAmount : discountPrice;
+ }
+
+ return {
+ discountPrice, // 折扣抵扣金额(即优惠的金额)
+ hasDiscountGoodsArr: [],
+ };
}
/**
@@ -307,39 +459,45 @@ export function returnCouponZhekouDiscount(
* @param canDikouGoodsArr 可抵扣商品列表
* @param coupon 优惠券
* @param user 用户信息
+ * @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-export function returnCouponProductDiscount(canDikouGoodsArr, coupon, user) {
- const {
- useFoods,
- discountNum,
- useRule
- } = coupon;
- //抵扣商品数组
- let discountGoodsArr = [];
- //抵扣全部商品
- if (useFoods.length === 0) {
- if (useRule == "price_asc") {
- discountGoodsArr = canDikouGoodsArr
- .slice(canDikouGoodsArr.length - discountNum, canDikouGoodsArr.length)
- .reverse();
- } else {
- discountGoodsArr = canDikouGoodsArr.slice(0, discountNum);
- }
- } else {
- //抵扣选中商品
- const discountSelGoodsArr = canDikouGoodsArr.filter((v) =>
- useFoods.find((food) => food.id == v.productId)
- );
- if (useRule == "price_asc") {
- discountGoodsArr = discountSelGoodsArr
- .slice(discountSelGoodsArr.length - discountNum, discountSelGoodsArr.length)
- .reverse();
- } else {
- discountGoodsArr = discountSelGoodsArr.slice(0, discountNum);
- }
- }
- const result = calcDiscountGoodsArrPrice(discountGoodsArr, discountNum, user);
- return result;
+export function returnCouponProductDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+) {
+ const { useFoods, discountNum, useRule } = coupon;
+ //抵扣商品数组
+ let discountGoodsArr = [];
+ //抵扣全部商品
+ if (useFoods.length === 0) {
+ if (useRule == "price_asc") {
+ discountGoodsArr = canDikouGoodsArr.slice(discountNum * -1).reverse();
+ } else {
+ discountGoodsArr = canDikouGoodsArr.slice(0, discountNum);
+ }
+ } else {
+ //抵扣选中商品
+ const discountSelGoodsArr = canDikouGoodsArr.filter((v) =>
+ useFoods.find((food) => food.id == v.productId)
+ );
+ if (useRule == "price_asc") {
+ discountGoodsArr = discountSelGoodsArr.slice(discountNum * -1).reverse();
+ } else {
+ discountGoodsArr = discountSelGoodsArr.slice(0, discountNum);
+ }
+ }
+ const result = calcDiscountGoodsArrPrice(
+ discountGoodsArr,
+ discountNum,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
+ return result;
}
// 返回买一送一券抵扣详情
@@ -348,38 +506,47 @@ export function returnCouponProductDiscount(canDikouGoodsArr, coupon, user) {
* @param coupon 优惠券
* @param user 用户信息
* @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-function returnCouponBuyOneGiveOneDiscount(canDikouGoodsArr, coupon, user,shopInfo) {
- const {
- useFoods,
- useRule
- } = coupon;
- //抵扣商品
- let discountGoods = undefined;
- //符合买一送一条件的商品
- const canUseGoods = canDikouGoodsArr.filter((v) => v.num >= 2);
- //抵扣全部商品
- if (useFoods.length === 0) {
- if (useRule == "price_asc") {
- discountGoods = canUseGoods[canUseGoods.length - 1];
- } else {
- discountGoods = canUseGoods.slice(0, 1);
- }
- } else {
- //符合抵扣条件的商品
- const canUseGoods1 = canUseGoods.filter((v) => useFoods.find((food) => food.id == v.productId));
- if (useRule == "price_asc") {
- discountGoods = canUseGoods1[canUseGoods1.length - 1];
- } else {
- discountGoods = canUseGoods1.slice(0, 1);
- }
- }
- const discountPrice = returnGoodsPrice(discountGoods, user,shopInfo);
- const hasDiscountGoodsArr = [discountGoods];
- return {
- discountPrice,
- hasDiscountGoodsArr
- };
+function returnCouponBuyOneGiveOneDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,limitTimeDiscount
+) {
+ const { useFoods, useRule } = coupon;
+ //抵扣商品
+ let discountGoods = undefined;
+ //符合买一送一条件的商品
+ const canUseGoods = canDikouGoodsArr.filter((v) => v.num >= 2);
+ //抵扣全部商品
+ if (useFoods.length === 0) {
+ if (useRule == "price_asc") {
+ discountGoods = canUseGoods[canUseGoods.length - 1];
+ } else {
+ discountGoods = canUseGoods[0];
+ }
+ } else {
+ //符合抵扣条件的商品
+ const canUseGoods1 = canUseGoods.filter((v) =>
+ useFoods.find((food) => food.id == v.productId)
+ );
+ if (useRule == "price_asc") {
+ discountGoods = canUseGoods1[canUseGoods1.length - 1];
+ } else {
+ discountGoods = canUseGoods1[0];
+ }
+ }
+ let discountPrice = 0;
+ let hasDiscountGoodsArr = [];
+ if (discountGoods) {
+ discountPrice = returnGoodsPrice(discountGoods, user, shopInfo,limitTimeDiscount);
+ hasDiscountGoodsArr = [discountGoods];
+ }
+ return {
+ discountPrice: discountPrice <= 0 ? 0 : discountPrice,
+ hasDiscountGoodsArr,
+ };
}
/**
@@ -388,39 +555,46 @@ function returnCouponBuyOneGiveOneDiscount(canDikouGoodsArr, coupon, user,shopIn
* @param coupon 优惠券
* @param user 用户信息
* @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-function returnSecoendDiscount(canDikouGoodsArr, coupon, user,shopInfo) {
- const {
- useFoods,
- useRule
- } = coupon;
- //抵扣商品
- let discountGoods = undefined;
- //符合买一送一条件的商品
- const canUseGoods = canDikouGoodsArr.filter((v) => v.num >= 2);
- //抵扣全部商品
- if (useFoods.length === 0) {
- if (useRule == "price_asc") {
- discountGoods = canUseGoods[canUseGoods.length - 1];
- } else {
- discountGoods = canUseGoods.slice(0, 1);
- }
- } else {
- //符合抵扣条件的商品
- const canUseGoods1 = canUseGoods.filter((v) => useFoods.find((food) => food.id == v.productId));
- if (useRule == "price_asc") {
- discountGoods = canUseGoods1[canUseGoods1.length - 1];
- } else {
- discountGoods = canUseGoods1.slice(0, 1);
- }
- }
- const discountPrice = returnGoodsPrice(discountGoods, user,shopInfo);
- const hasDiscountGoodsArr = [discountGoods];
- //返回半价价格
- return {
- discountPrice: new BigNumber(discountPrice).dividedBy(2).toNumber(),
- hasDiscountGoodsArr,
- };
+function returnSecoendDiscount(canDikouGoodsArr, coupon, user, shopInfo,limitTimeDiscount) {
+ const { useFoods, useRule } = coupon;
+ //抵扣商品
+ let discountGoods = undefined;
+ //符合条件的商品
+ const canUseGoods = canDikouGoodsArr.filter((v) => v.num >= 2);
+ //抵扣全部商品
+ if (useFoods.length === 0) {
+ if (useRule == "price_asc") {
+ discountGoods = canUseGoods[canUseGoods.length - 1];
+ } else {
+ discountGoods = canUseGoods[0];
+ }
+ } else {
+ //符合抵扣条件的商品
+ const canUseGoods1 = canUseGoods.filter((v) =>
+ useFoods.find((food) => food.id == v.productId)
+ );
+ if (useRule == "price_asc") {
+ discountGoods = canUseGoods1[canUseGoods1.length - 1];
+ } else {
+ discountGoods = canUseGoods1[0];
+ }
+ }
+ let discountPrice = 0;
+ let hasDiscountGoodsArr = [];
+ if (discountGoods) {
+ discountPrice = returnGoodsPrice(discountGoods, user, shopInfo,limitTimeDiscount);
+ hasDiscountGoodsArr = [discountGoods];
+ }
+ //返回半价价格
+ return {
+ discountPrice:
+ discountPrice <= 0
+ ? 0
+ : new BigNumber(discountPrice).dividedBy(2).toNumber(),
+ hasDiscountGoodsArr,
+ };
}
/**
@@ -428,17 +602,21 @@ function returnSecoendDiscount(canDikouGoodsArr, coupon, user,shopInfo) {
* @param arr 商品列表
* @param user 用户信息
* @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-export function returnCanDikouGoods(arr, user, shopInfo) {
- const result = arr
- .filter((v) => {
- return v.is_temporary != 1 && v.is_gift != 1;
- })
- .filter((v) => {
- return v.num > 0;
- })
- .sort((a, b) => {
- return returnGoodsPrice(b, use, shopInfo) - returnGoodsPrice(a, user, shopInfo);
- });
- return result;
-}
\ No newline at end of file
+export function returnCanDikouGoods(arr, user, shopInfo,limitTimeDiscount) {
+ const result = arr
+ .filter((v) => {
+ return v.is_temporary != 1 && v.is_gift != 1;
+ })
+ .filter((v) => {
+ return v.num > 0;
+ })
+ .sort((a, b) => {
+ return (
+ returnGoodsPrice(b, user, shopInfo,limitTimeDiscount) -
+ returnGoodsPrice(a, user, shopInfo,limitTimeDiscount)
+ );
+ });
+ return result;
+}
diff --git a/utils/goods-utils.js b/utils/goods-utils.js
index cb454ab..37a8114 100644
--- a/utils/goods-utils.js
+++ b/utils/goods-utils.js
@@ -5,19 +5,45 @@ import _ from "lodash";
* 返回商品单价
* @param goods 商品
* @param user 用户信息
- * @param {Object} shopInfo
+ * @param {Object} shopInfo 店铺信息
+ * @param {boolean} limitTimeDiscount - 限时折扣
*/
-export function returnGoodsPrice(goods, user, shopInfo) {
+export function returnGoodsPrice(goods, user, shopInfo, limitTimeDiscount) {
if (!goods) {
return 0;
}
+ //是否可以使用会员价
+ const canUseVipPrice =
+ user && user.isVip && user.isMemberPrice && goods.memberPrice * 1 > 0 && shopInfo && shopInfo.isMemberPrice;
+ // 商家改价
if (goods.discount_sale_amount * 1 > 0) {
- return goods.discount_sale_amount;
- }
- if (shopInfo && !shopInfo.isMemberPrice) {
return goods.salePrice;
}
- if (user.isVip && goods.memberPrice * 1 <= goods.salePrice * 1 && goods.memberPrice * 1 > 0) {
+ // 限时折扣
+ if (limitTimeDiscount && limitTimeDiscount.id) {
+ const canUseFoods = limitTimeDiscount.foods.split(",");
+ const canUseLimit =
+ limitTimeDiscount.foodType == 1 ||
+ canUseFoods.includes(`${goods.productId}`);
+ if (canUseLimit && limitTimeDiscount.discountPriority == "limit-time") {
+ return new BigNumber(goods.salePrice)
+ .times(limitTimeDiscount.discountRate / 100)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ }
+
+ if (canUseLimit && limitTimeDiscount.discountPriority == "vip-price") {
+ if (canUseVipPrice) {
+ return goods.memberPrice;
+ } else {
+ return new BigNumber(goods.salePrice)
+ .times(limitTimeDiscount.discountRate / 100)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ }
+ }
+ }
+ if (canUseVipPrice) {
return goods.memberPrice;
}
return goods.salePrice;
@@ -63,18 +89,23 @@ export function returnCoupType(coupon) {
* @param selCoupon 已选择的优惠券列表
* @param user 用户信息
*/
-export function returnCanDikouGoodsArr(canDikouGoodsArr, selCoupon, user) {
+export function returnCanDikouGoodsArr(args) {
+ const { canDikouGoodsArr, selCoupon, user, shopInfo, limitTimeDiscount } =
+ args;
const types = [2, 4, 6];
// 收集已抵扣商品并关联对应的优惠券类型
const goodsCouponGoods = selCoupon
.filter((v) => types.includes(v.type))
.reduce((prev, cur) => {
// 给每个抵扣商品添加所属优惠券类型
- const goodsWithType = cur.discount.hasDiscountGoodsArr.map((goods) => ({
- ...goods,
- couponType: cur.type, // 记录该商品是被哪种类型的优惠券抵扣的
- }));
- prev.push(...goodsWithType);
+ if(cur&&cur.discount){
+ const goodsWithType = cur.discount.hasDiscountGoodsArr.map((goods) => ({
+ ...goods,
+ couponType: cur.type, // 记录该商品是被哪种类型的优惠券抵扣的
+ }));
+ prev.push(...goodsWithType);
+ }
+
return prev;
}, []);
const arr = _.cloneDeep(canDikouGoodsArr)
@@ -92,11 +123,60 @@ export function returnCanDikouGoodsArr(canDikouGoodsArr, selCoupon, user) {
}
return v;
})
- .filter((v) => v.num > 0); // 过滤掉数量<=0的商品
+ .filter((v) => {
+ const canUseNum = v.num - (v.returnNum || 0);
+ if (canUseNum <= 0 || v.is_temporary == 1 || v.is_gift == 1) {
+ return false;
+ }
+
+ return true;
+ }); // 过滤掉数量<=0的商品,赠菜,临时菜
return arr;
}
+/**
+ * 返回商品是否享用了会员价/会员折扣
+ * @param {*} goods
+ */
+function returnGoodsIsUseVipPrice(shopInfo, user, goods) {
+ if (goods.is_time_discount) {
+ return false;
+ }
+ if (shopInfo.isMemberPrice != 1 || user.isVip != 1) {
+ return false;
+ }
+ if (shopInfo.isMemberPrice == 1 && user.isVip == 1) {
+ if (goods.memberPrice <= 0) {
+ return false;
+ }
+ return true;
+ }
+}
+
+/**
+ * 返回可以计算抵扣金额的商品列表
+ */
+function returnCanCalcGoodsList(canCalcGoodsArr, coupon, shopInfo, user) {
+ return canCalcGoodsArr.filter((goods) => {
+ console.log("goods");
+ console.log(goods);
+ if (
+ !coupon.discountShare &&
+ (goods.is_time_discount || goods.isTimeDiscount)
+ ) {
+ return false;
+ }
+ if (
+ !coupon.vipPriceShare &&
+ returnGoodsIsUseVipPrice(shopInfo, user, goods)
+ ) {
+ return false;
+ }
+ return true;
+ });
+}
+
/**
* 判断优惠券是否可使用,并返回不可用原因
*
@@ -111,10 +191,20 @@ export function returnCanDikouGoodsArr(canDikouGoodsArr, selCoupon, user) {
* @param {Object} args.user - 用户信息对象
* @param {Object} args.selCoupon - 已经选择的优惠券信息对象
* @param {Object} args.shopInfo
+ * @param {boolean} args.limitTimeDiscount - 限时折扣
* @returns {Object} - { canUse: boolean, reason: string } 可用状态及不可用原因
*/
export function returnCouponCanUse(args) {
- let { canDikouGoodsArr, coupon, goodsOrderPrice, user, selCoupon, shopInfo } = args;
+ let {
+ canDikouGoodsArr,
+ coupon,
+ goodsOrderPrice,
+ user,
+ selCoupon,
+ shopInfo,
+ isMemberPrice,
+ limitTimeDiscount,
+ } = args;
// 优惠券未启用
if (!coupon.use) {
return {
@@ -122,10 +212,27 @@ export function returnCouponCanUse(args) {
reason: coupon.noUseRestrictions || "不在可用时间段内",
};
}
+ if (
+ limitTimeDiscount &&
+ limitTimeDiscount.id &&
+ limitTimeDiscount.foodType == 1 &&
+ !coupon.discountShare
+ ) {
+ return {
+ canUse: false,
+ reason: coupon.noUseRestrictions || "不可与限时折扣同享",
+ };
+ }
// 计算门槛金额
let fullAmount = goodsOrderPrice;
- canDikouGoodsArr = returnCanDikouGoodsArr(canDikouGoodsArr, selCoupon, user, shopInfo);
+ canDikouGoodsArr = returnCanDikouGoodsArr({
+ canDikouGoodsArr,
+ selCoupon,
+ user,
+ shopInfo,
+ limitTimeDiscount,
+ });
//优惠券指定门槛商品列表
let canCalcGoodsArr = [...canDikouGoodsArr];
//部分商品参与门槛计算
@@ -133,10 +240,20 @@ export function returnCouponCanUse(args) {
canCalcGoodsArr = canDikouGoodsArr.filter((v) => {
return coupon.thresholdFoods.find((food) => food.id == v.productId);
});
- fullAmount = canCalcGoodsArr.reduce((pre, cur) => {
- return pre + returnGoodsPrice(cur, user, shopInfo) * cur.num;
- }, 0);
}
+ canCalcGoodsArr = returnCanCalcGoodsList(
+ canCalcGoodsArr,
+ coupon,
+ shopInfo,
+ user
+ );
+ console.log("canCalcGoodsArr");
+ console.log(canCalcGoodsArr);
+ fullAmount = canCalcGoodsArr.reduce((pre, cur) => {
+ return (
+ pre + returnGoodsPrice(cur, user, shopInfo, limitTimeDiscount) * cur.num
+ );
+ }, 0);
// 是否全部商品可用
const isDikouAll = coupon.useFoods.length === 0;
@@ -147,12 +264,12 @@ export function returnCouponCanUse(args) {
return coupon.useFoods.find((food) => food.id == v.productId);
});
}
- if (user.isVip && !coupon.vipPriceShare) {
- return {
- canUse: false,
- reason: "非会员可用",
- };
- }
+ // if (user.isVip && !coupon.vipPriceShare) {
+ // return {
+ // canUse: false,
+ // reason: "非会员可用",
+ // };
+ // }
if (selCoupon.length > 0 && !selCoupon[0].otherCouponShare) {
return {
canUse: false,
@@ -181,17 +298,8 @@ export function returnCouponCanUse(args) {
};
}
}
-
// 商品兑换券,第二件半价和买一送一判断是否有可用商品
if ([2, 4, 5].includes(coupon.type)) {
- console.log("商品兑换券", fullAmount, coupon.fullAmount);
- if (coupon.type == 2 && fullAmount < coupon.fullAmount) {
- console.log("商品兑换券", coupon);
- return {
- canUse: false,
- reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`,
- };
- }
// 没有符合条件的商品
if (isDikouAll && canDikouGoodsArr.length === 0) {
return {
@@ -205,6 +313,20 @@ export function returnCouponCanUse(args) {
reason: "没有符合条件的商品",
};
}
+ if (coupon.type == 2) {
+ if (canCalcGoodsArr.length <= 0) {
+ return {
+ canUse: false,
+ reason: "没有符合计算门槛条件的商品",
+ };
+ }
+ if (fullAmount < coupon.fullAmount) {
+ return {
+ canUse: false,
+ reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`,
+ };
+ }
+ }
}
//商品兑换券是否达到门槛金额
if (coupon.type == 2 && goodsOrderPrice < coupon.fullAmount) {
@@ -219,8 +341,8 @@ export function returnCouponCanUse(args) {
let canUse = false;
if (isDikouAll) {
canUse = canDikouGoodsArr.some((v) => v.num >= 2);
- } else if (canCalcGoodsArr.length > 0) {
- canUse = canCalcGoodsArr.some((v) => v.num >= 2);
+ } else if (canUseGoodsArr.length > 0) {
+ canUse = canUseGoodsArr.some((v) => v.num >= 2);
}
if (!canUse) {
@@ -236,10 +358,9 @@ export function returnCouponCanUse(args) {
let canUse = false;
if (isDikouAll) {
canUse = canDikouGoodsArr.some((v) => v.num >= 2);
- } else if (canCalcGoodsArr.length > 0) {
- canUse = canCalcGoodsArr.some((v) => v.num >= 2);
+ } else if (canUseGoodsArr.length > 0) {
+ canUse = canUseGoodsArr.some((v) => v.num >= 2);
}
-
if (!canUse) {
return {
canUse: false,
@@ -261,11 +382,19 @@ export function returnCouponCanUse(args) {
* @param discountNum 抵扣数量
* @param user 用户信息
* @param {Object} shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-export function calcDiscountGoodsArrPrice(discountGoodsArr, discountNum, user, shopInfo) {
+export function calcDiscountGoodsArrPrice(
+ discountGoodsArr,
+ discountNum,
+ user,
+ shopInfo,
+ limitTimeDiscount
+) {
let hasCountNum = 0;
let discountPrice = 0;
let hasDiscountGoodsArr = [];
+
for (let i = 0; i < discountGoodsArr.length; i++) {
if (hasCountNum >= discountNum) {
break;
@@ -273,7 +402,14 @@ export function calcDiscountGoodsArrPrice(discountGoodsArr, discountNum, user, s
const goods = discountGoodsArr[i];
const shengyuNum = discountNum - hasCountNum;
const num = Math.min(goods.num, shengyuNum);
- discountPrice += returnGoodsPrice(goods, user, shopInfo) * num;
+ const realPrice = returnGoodsPrice(
+ goods,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
+
+ discountPrice += realPrice * num;
hasCountNum += num;
hasDiscountGoodsArr.push({
@@ -296,21 +432,62 @@ export function calcDiscountGoodsArrPrice(discountGoodsArr, discountNum, user, s
* @param goodsOrderPrice 商品订单金额
* @param selCoupon 已选择的优惠券列表
* @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-export function returnCouponDiscount(arr, coupon, user, goodsOrderPrice, selCoupon, shopInfo) {
- const canDikouGoodsArr = returnCanDikouGoodsArr(arr, selCoupon, user);
+export function returnCouponDiscount(
+ arr,
+ coupon,
+ user,
+ goodsOrderPrice,
+ selCoupon,
+ shopInfo,
+ limitTimeDiscount
+) {
+ arr = returnCanDikouGoods(arr, user, shopInfo,limitTimeDiscount);
+ const canDikouGoodsArr = returnCanDikouGoodsArr({
+ canDikouGoodsArr: arr,
+ selCoupon,
+ user,
+ shopInfo,
+ limitTimeDiscount,
+ });
if (coupon.type == 2) {
- return returnCouponProductDiscount(canDikouGoodsArr, coupon, user, shopInfo);
+ return returnCouponProductDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
}
if (coupon.type == 6) {
- const result = returnCouponBuyOneGiveOneDiscount(canDikouGoodsArr, coupon, user, shopInfo);
+ const result = returnCouponBuyOneGiveOneDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
return result;
}
if (coupon.type == 4) {
- return returnSecoendDiscount(canDikouGoodsArr, coupon, user, shopInfo);
+ return returnSecoendDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
}
if (coupon.type == 3) {
- return returnCouponZhekouDiscount(canDikouGoodsArr, coupon, user, goodsOrderPrice, selCoupon);
+ return returnCouponZhekouDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ goodsOrderPrice,
+ selCoupon,
+ limitTimeDiscount
+ );
}
}
@@ -321,6 +498,7 @@ export function returnCouponDiscount(arr, coupon, user, goodsOrderPrice, selCoup
* @param user 用户信息
* @param goodsOrderPrice 商品订单金额
* @param selCoupon 已选择的优惠券列表
+ * @param limitTimeDiscount 限时折扣
*
*/
export function returnCouponZhekouDiscount(
@@ -328,7 +506,8 @@ export function returnCouponZhekouDiscount(
coupon,
user,
goodsOrderPrice,
- selCoupon
+ selCoupon,
+ limitTimeDiscount
) {
const { discountRate, maxDiscountAmount } = coupon;
@@ -336,14 +515,20 @@ export function returnCouponZhekouDiscount(
const goodsCouponDiscount = selCoupon
.filter((v) => v.type == 2)
.reduce((prve, cur) => {
- return new BigNumber(prve).plus(new BigNumber(cur.discount.discountPrice));
+ return new BigNumber(prve).plus(
+ new BigNumber(cur.discount.discountPrice)
+ );
}, new BigNumber(0));
// 将商品订单价格转换为BigNumber并减去优惠券折扣
- const adjustedGoodsOrderPrice = new BigNumber(goodsOrderPrice).minus(goodsCouponDiscount);
+ const adjustedGoodsOrderPrice = new BigNumber(goodsOrderPrice).minus(
+ goodsCouponDiscount
+ );
// 计算优惠比例:(100 - 折扣率) / 100
- const discountAmountRatio = new BigNumber(100).minus(discountRate).dividedBy(100);
+ const discountAmountRatio = new BigNumber(100)
+ .minus(discountRate)
+ .dividedBy(100);
// 计算折扣金额:调整后的商品订单金额 × 优惠比例
let discountPrice = adjustedGoodsOrderPrice
@@ -353,7 +538,8 @@ export function returnCouponZhekouDiscount(
// 应用最大折扣金额限制
if (maxDiscountAmount !== 0) {
- discountPrice = discountPrice >= maxDiscountAmount ? maxDiscountAmount : discountPrice;
+ discountPrice =
+ discountPrice >= maxDiscountAmount ? maxDiscountAmount : discountPrice;
}
return {
@@ -368,11 +554,20 @@ export function returnCouponZhekouDiscount(
* @param coupon 优惠券
* @param user 用户信息
* @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-export function returnCouponProductDiscount(canDikouGoodsArr, coupon, user, shopInfo) {
+export function returnCouponProductDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+) {
const { useFoods, discountNum, useRule } = coupon;
+
//抵扣商品数组
let discountGoodsArr = [];
+
//抵扣全部商品
if (useFoods.length === 0) {
if (useRule == "price_asc") {
@@ -391,7 +586,15 @@ export function returnCouponProductDiscount(canDikouGoodsArr, coupon, user, shop
discountGoodsArr = discountSelGoodsArr.slice(0, discountNum);
}
}
- const result = calcDiscountGoodsArrPrice(discountGoodsArr, discountNum, user, shopInfo);
+
+
+ const result = calcDiscountGoodsArrPrice(
+ discountGoodsArr,
+ discountNum,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
return result;
}
@@ -401,8 +604,15 @@ export function returnCouponProductDiscount(canDikouGoodsArr, coupon, user, shop
* @param coupon 优惠券
* @param user 用户信息
* @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-function returnCouponBuyOneGiveOneDiscount(canDikouGoodsArr, coupon, user, shopInfo) {
+function returnCouponBuyOneGiveOneDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+) {
const { useFoods, useRule } = coupon;
//抵扣商品
let discountGoods = undefined;
@@ -417,7 +627,9 @@ function returnCouponBuyOneGiveOneDiscount(canDikouGoodsArr, coupon, user, shopI
}
} else {
//符合抵扣条件的商品
- const canUseGoods1 = canUseGoods.filter((v) => useFoods.find((food) => food.id == v.productId));
+ const canUseGoods1 = canUseGoods.filter((v) =>
+ useFoods.find((food) => food.id == v.productId)
+ );
if (useRule == "price_asc") {
discountGoods = canUseGoods1[canUseGoods1.length - 1];
} else {
@@ -426,9 +638,13 @@ function returnCouponBuyOneGiveOneDiscount(canDikouGoodsArr, coupon, user, shopI
}
let discountPrice = 0;
let hasDiscountGoodsArr = [];
- console.log("returnCouponBuyOneGiveOneDiscount:discountGoods", discountGoods);
if (discountGoods) {
- discountPrice = returnGoodsPrice(discountGoods, user, shopInfo);
+ discountPrice = returnGoodsPrice(
+ discountGoods,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
hasDiscountGoodsArr = [discountGoods];
}
return {
@@ -443,8 +659,15 @@ function returnCouponBuyOneGiveOneDiscount(canDikouGoodsArr, coupon, user, shopI
* @param coupon 优惠券
* @param user 用户信息
* @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-function returnSecoendDiscount(canDikouGoodsArr, coupon, user, shopInfo) {
+function returnSecoendDiscount(
+ canDikouGoodsArr,
+ coupon,
+ user,
+ shopInfo,
+ limitTimeDiscount
+) {
const { useFoods, useRule } = coupon;
//抵扣商品
let discountGoods = undefined;
@@ -459,7 +682,9 @@ function returnSecoendDiscount(canDikouGoodsArr, coupon, user, shopInfo) {
}
} else {
//符合抵扣条件的商品
- const canUseGoods1 = canUseGoods.filter((v) => useFoods.find((food) => food.id == v.productId));
+ const canUseGoods1 = canUseGoods.filter((v) =>
+ useFoods.find((food) => food.id == v.productId)
+ );
if (useRule == "price_asc") {
discountGoods = canUseGoods1[canUseGoods1.length - 1];
} else {
@@ -469,12 +694,20 @@ function returnSecoendDiscount(canDikouGoodsArr, coupon, user, shopInfo) {
let discountPrice = 0;
let hasDiscountGoodsArr = [];
if (discountGoods) {
- discountPrice = returnGoodsPrice(discountGoods, user, shopInfo);
+ discountPrice = returnGoodsPrice(
+ discountGoods,
+ user,
+ shopInfo,
+ limitTimeDiscount
+ );
hasDiscountGoodsArr = [discountGoods];
}
//返回半价价格
return {
- discountPrice: discountPrice <= 0 ? 0 : new BigNumber(discountPrice).dividedBy(2).toNumber(),
+ discountPrice:
+ discountPrice <= 0
+ ? 0
+ : new BigNumber(discountPrice).dividedBy(2).toNumber(),
hasDiscountGoodsArr,
};
}
@@ -484,8 +717,9 @@ function returnSecoendDiscount(canDikouGoodsArr, coupon, user, shopInfo) {
* @param arr 商品列表
* @param user 用户信息
* @param shopInfo 店铺信息
+ * @param limitTimeDiscount 限时折扣
*/
-export function returnCanDikouGoods(arr, user, shopInfo) {
+export function returnCanDikouGoods(arr, user, shopInfo, limitTimeDiscount) {
const result = arr
.filter((v) => {
return v.is_temporary != 1 && v.is_gift != 1;
@@ -494,7 +728,10 @@ export function returnCanDikouGoods(arr, user, shopInfo) {
return v.num > 0;
})
.sort((a, b) => {
- return returnGoodsPrice(b, user, shopInfo) - returnGoodsPrice(a, user, shopInfo);
+ return (
+ returnGoodsPrice(b, user, shopInfo, limitTimeDiscount) -
+ returnGoodsPrice(a, user, shopInfo, limitTimeDiscount)
+ );
});
return result;
}
diff --git a/utils/goods.ts b/utils/goods.ts
new file mode 100644
index 0000000..e693399
--- /dev/null
+++ b/utils/goods.ts
@@ -0,0 +1,1709 @@
+import { BigNumber } from "bignumber.js";
+
+// 配置BigNumber精度
+BigNumber.set({
+ DECIMAL_PLACES: 2,
+ ROUNDING_MODE: BigNumber.ROUND_DOWN, // 向下取整,符合业务需求
+});
+
+/**
+ * 购物车订单价格计算公共库
+ * 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展
+ * 小数处理:使用bignumber.js确保精度,统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99)
+ * 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑
+ * 关键规则:
+ * - 所有优惠券均支持指定门槛商品(后端foods字段:空字符串=全部商品,ID字符串=指定商品ID)
+ * - 与限时折扣/会员价同享规则:开启则门槛计算含对应折扣,关闭则用原价/非会员价
+ * 字段说明:
+ * - BaseCartItem.id:购物车项ID(唯一标识购物车中的条目)
+ * - BaseCartItem.product_id:商品ID(唯一标识商品,用于优惠券/活动匹配)
+ * - BaseCartItem.skuData.id:SKU ID(唯一标识商品规格)
+ */
+
+// ============================ 1. 基础类型定义(核心修正:明确ID含义) ============================
+/** 商品类型枚举 */
+export enum GoodsType {
+ NORMAL = "normal", // 普通商品
+ WEIGHT = "weight", // 称重商品
+ GIFT = "gift", // 赠菜(继承普通商品逻辑,标记用)
+ EMPTY = "", // 空字符串类型(后端未返回时默认归类为普通商品)
+ PACKAGE = "package", // 打包商品(如套餐/预打包商品,按普通商品逻辑处理,可扩展特殊规则)
+}
+
+/** 优惠券计算结果类型(新增细分字段) */
+interface CouponResult {
+ deductionAmount: number; // 抵扣金额
+ excludedProductIds: string[]; // 不适用商品ID列表(注意:是商品ID,非购物车ID)
+ usedCoupon: Coupon | undefined; // 实际使用的优惠券
+ productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券等)
+ fullCouponDeduction: number; // 新增:满减优惠券抵扣
+}
+
+/** 兑换券计算结果类型(新增细分字段) */
+interface ExchangeCalculationResult {
+ deductionAmount: number;
+ excludedProductIds: string[]; // 不适用商品ID列表(商品ID)
+ productCouponDeduction: number; // 新增:兑换券属于商品券,同步记录
+}
+
+/** 优惠券类型枚举 */
+export enum CouponType {
+ FULL_REDUCTION = "full_reduction", // 满减券
+ DISCOUNT = "discount", // 折扣券
+ SECOND_HALF = "second_half", // 第二件半价券
+ BUY_ONE_GET_ONE = "buy_one_get_one", // 买一送一券
+ EXCHANGE = "exchange", // 商品兑换券
+}
+
+/** 后端返回的优惠券原始字段类型 */
+export interface BackendCoupon {
+ id?: number; // 自增主键(int64)
+ shopId?: number; // 店铺ID(int64)
+ syncId?: number; // 同步Id(int64)
+ type?: number; // 优惠券类型:1-满减券,2-商品兑换券,3-折扣券,4-第二件半价券,5-消费送券,6-买一送一券,7-固定价格券,8-免配送费券
+ name?: string; // 券名称
+ useShopType?: string; // 可用门店类型:only-仅本店;all-所有门店,custom-指定门店
+ useShops?: string; // 可用门店(逗号分隔字符串,如"1,2,3")
+ useType?: string; // 可使用类型:dine堂食/pickup自取/deliv配送/express快递
+ validType?: string; // 有效期类型:fixed(固定时间),custom(自定义时间)
+ validDays?: number; // 有效期(天)
+ validStartTime?: string; // 有效期开始时间(如"2024-01-01 00:00:00")
+ validEndTime?: string; // 有效期结束时间
+ daysToTakeEffect?: number; // 隔天生效
+ useDays?: string; // 可用周期(如"周一,周二")
+ useTimeType?: string; // 可用时间段类型:all-全时段,custom-指定时段
+ useStartTime?: string; // 可用开始时间(每日)
+ useEndTime?: string; // 可用结束时间(每日)
+ getType?: string; // 发放设置:不可自行领取/no,可领取/yes
+ getMode?: string; // 用户领取方式
+ giveNum?: number; // 总发放数量,-10086为不限量
+ getUserType?: string; // 可领取用户:全部/all,新用户一次/new,仅会员/vip
+ getLimit?: number; // 每人领取限量,-10086为不限量
+ useLimit?: number; // 每人每日使用限量,-10086为不限量
+ discountShare?: number; // 与限时折扣同享:0-否,1-是
+ vipPriceShare?: number; // 与会员价同享:0-否,1-是
+ ruleDetails?: string; // 附加规则说明
+ status?: number; // 状态:0-禁用,1-启用
+ useNum?: number; // 已使用数量
+ leftNum?: number; // 剩余数量
+ foods?: string; // 指定门槛商品(逗号分隔字符串,如"101,102",此处为商品ID)
+ fullAmount?: number; // 使用门槛:满多少金额(元)
+ discountAmount?: number; // 使用门槛:减多少金额(元)
+ discountRate?: number; // 折扣%(如90=9折)
+ maxDiscountAmount?: number; // 可抵扣最大金额(元)
+ useRule?: string; // 使用规则:price_asc-价格低到高,price_desc-高到低
+ discountNum?: number; // 抵扣数量
+ otherCouponShare?: number; // 与其它优惠共享:0-否,1-是
+ createTime?: string; // 创建时间
+ updateTime?: string; // 更新时间
+}
+
+/** 营销活动类型枚举 */
+export enum ActivityType {
+ TIME_LIMIT_DISCOUNT = "time_limit_discount", // 限时折扣
+}
+
+/** 基础购物车商品项(核心修正:新增product_id,明确各ID含义) */
+export interface BaseCartItem {
+ id: string | number; // 购物车ID(唯一标识购物车中的条目,如购物车项主键)
+ product_id: string | number; // 商品ID(唯一标识商品,用于优惠券/活动匹配,必选)
+ salePrice: number; // 商品原价(元)
+ number: number; // 商品数量
+ product_type: GoodsType; // 商品类型
+ is_temporary?: boolean; // 是否临时菜(默认false)
+ is_gift?: boolean; // 是否赠菜(默认false)
+ returnNum?: number; // 退货数量(历史订单用,默认0)
+ memberPrice?: number; // 商品会员价(元,优先级:商品会员价 > 会员折扣)
+ discountSaleAmount?: number; // 商家改价后单价(元,优先级最高)
+ packFee?: number; // 单份打包费(元,默认0)
+ packNumber?: number; // 堂食打包数量(默认0)
+ activityInfo?: {
+ // 商品参与的营销活动(如限时折扣)
+ type: ActivityType;
+ discountRate: number; // 折扣率(如0.8=8折)
+ vipPriceShare: boolean; // 是否与会员优惠同享(默认false)
+ };
+ skuData?: {
+ // SKU扩展数据(可选)
+ id: string | number; // SKU ID(唯一标识商品规格,如颜色/尺寸)
+ memberPrice?: number; // SKU会员价
+ salePrice?: number; // SKU原价
+ };
+}
+
+/** 基础优惠券接口(所有券类型继承,包含统一门槛商品字段) */
+export interface BaseCoupon {
+ id: string | number; // 优惠券ID
+ type: CouponType; // 工具库字符串枚举(由后端couponType转换)
+ name: string; // 对应后端title
+ available: boolean; // 基于BackendCoupon字段计算的可用性
+ useShopType?: string; // only-仅本店;all-所有门店,custom-指定门店
+ useShops: string[]; // 可用门店ID列表
+ discountShare: boolean; // 与限时折扣同享:0-否,1-是(后端字段转换为布尔值)
+ vipPriceShare: boolean; // 与会员价同享:0-否,1-是(后端字段转换为布尔值)
+ useType?: string[]; // 可使用类型:dine堂食/pickup自取/deliv配送/express快递
+ isValid: boolean; // 是否在有效期内
+ discountAmount?: number; // 减免金额 (满减券有)
+ fullAmount?: number; // 使用门槛:满多少金额
+ maxDiscountAmount?: number; // 可抵扣最大金额 元
+ applicableProductIds: string[]; // 门槛商品ID列表(空数组=全部商品,非空=指定商品ID)
+}
+
+/** 满减券(适配后端字段) */
+export interface FullReductionCoupon extends BaseCoupon {
+ type: CouponType.FULL_REDUCTION;
+ fullAmount: number; // 对应后端fullAmount(满减门槛)
+ discountAmount: number; // 对应后端discountAmount(减免金额)
+ maxDiscountAmount?: number; // 对应后端maxDiscountAmount(最大减免)
+}
+
+/** 折扣券(适配后端字段) */
+export interface DiscountCoupon extends BaseCoupon {
+ type: CouponType.DISCOUNT;
+ discountRate: number; // 后端discountRate(%)转小数(如90→0.9)
+ maxDiscountAmount: number; // 对应后端maxDiscountAmount(最大减免)
+}
+
+/** 第二件半价券(适配后端字段) */
+export interface SecondHalfPriceCoupon extends BaseCoupon {
+ type: CouponType.SECOND_HALF;
+ maxUseCountPerOrder?: number; // 对应后端useLimit(-10086=不限)
+}
+
+/** 买一送一券(适配后端字段) */
+export interface BuyOneGetOneCoupon extends BaseCoupon {
+ type: CouponType.BUY_ONE_GET_ONE;
+ maxUseCountPerOrder?: number; // 对应后端useLimit(-10086=不限)
+}
+
+/** 商品兑换券(适配后端字段) */
+export interface ExchangeCoupon extends BaseCoupon {
+ type: CouponType.EXCHANGE;
+ deductCount: number; // 对应后端discountNum(抵扣数量)
+ sortRule: "low_price_first" | "high_price_first"; // 后端useRule转换
+}
+
+/** 所有优惠券类型联合 */
+export type Coupon =
+ | FullReductionCoupon
+ | DiscountCoupon
+ | SecondHalfPriceCoupon
+ | BuyOneGetOneCoupon
+ | ExchangeCoupon;
+
+/** 营销活动配置(如限时折扣,applicableProductIds为商品ID列表) */
+export interface ActivityConfig {
+ type: ActivityType;
+ applicableProductIds?: string[]; // 适用商品ID列表(与BaseCartItem.product_id匹配)
+ discountRate: number; // 折扣率(如0.8=8折)
+ vipPriceShare: boolean; // 是否与会员优惠同享
+}
+
+/** 积分抵扣规则 */
+export interface PointDeductionRule {
+ pointsPerYuan: number; // X积分=1元(如100=100积分抵1元)
+ maxDeductionAmount?: number; // 最大抵扣金额(元,默认不限)
+}
+
+/** 餐位费配置 */
+export interface SeatFeeConfig {
+ pricePerPerson: number; // 每人餐位费(元)
+ personCount: number; // 用餐人数(默认1)
+ isEnabled: boolean; // 是否启用餐位费(默认false)
+}
+/** 商家减免类型枚举 */
+export enum MerchantReductionType {
+ FIXED_AMOUNT = "fixed_amount", // 固定金额减免(如直接减 10 元)
+ DISCOUNT_RATE = "discount_rate", // 比例折扣减免(如打 9 折,即减免 10%)
+}
+
+/** 商家减免配置(新增,替代原单一金额字段) */
+export interface MerchantReductionConfig {
+ type: MerchantReductionType; // 减免类型(二选一)
+ fixedAmount?: number; // 固定减免金额(元,仅 FIXED_AMOUNT 生效,≥0)
+ discountRate?: number; // 折扣率(%,仅 DISCOUNT_RATE 生效,0-100,如 90 代表 9 折)
+}
+/**商家霸王餐配置 */
+export interface FreeDineConfig {
+ enable: boolean; //是否开启
+ rechargeThreshold: number; //订单满多少元可以使用
+ rechargeTimes: number; //充值多少倍免单
+ withCoupon: boolean; //与优惠券同享
+ withPoints: boolean; //与积分同享
+ useType?: string[]; //使用类型 dine-in店内 takeout 自取 post快递,takeaway外卖
+ useShopType?: string; //all 全部 part部分
+ shopIdList?: number[]; //可用门店id
+}
+
+//限时折扣配置
+export interface TimeLimitDiscountConfig {
+ /**
+ * 折扣优先级 limit-time/vip-price
+ */
+ discountPriority: string;
+ /**
+ * 折扣% 范围1-99
+ */
+ discountRate: number;
+ /**
+ * 参与商品
+ */
+ foods: string;
+ /**
+ * 参与商品 1全部 2部分
+ */
+ foodType: number;
+ /**
+ * 自增主键
+ */
+ id: number;
+ /**
+ * 店铺ID
+ */
+ shopId: number;
+ /**
+ * 可使用类型:堂食 dine-in 外带 take-out 外卖 take-away 配送 post
+ */
+ useType: string;
+ [property: string]: any;
+}
+
+//用户信息
+interface ShopUserInfo {
+ isVip: number | null; //是否会员
+ discount: number | null; //用户折扣
+ isMemberPrice: number | null; //会员折扣与会员价是否同时使用
+}
+/** 订单额外费用配置 */
+export interface OrderExtraConfig {
+ // merchantReduction: number; // 商家减免金额(元,默认0)
+ // 替换原单一金额字段,支持两种减免形式
+ merchantReduction: MerchantReductionConfig;
+ additionalFee: number; // 附加费(元,如余额充值、券包,默认0)
+ pointDeductionRule: PointDeductionRule; // 积分抵扣规则
+ seatFeeConfig: SeatFeeConfig; // 餐位费配置
+ currentStoreId: string; // 当前门店ID(用于验证优惠券适用门店)
+ userPoints: number; // 用户当前积分(用于积分抵扣)
+ isMember: boolean; // 用户是否会员(用于会员优惠)
+ memberDiscountRate?: number; // 会员折扣率(如0.95=95折,无会员价时用)
+ newUserDiscount?: number; // 新用户减免金额(元,默认0)
+ fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构)
+ currentDinnerType: "dine-in" | "take-out" | "take-away" | "post"; // 当前就餐类型(匹配useType)
+ isFreeDine?: boolean; //是否霸王餐
+ freeDineConfig?: FreeDineConfig;
+ limitTimeDiscount?: TimeLimitDiscountConfig; //限时折扣
+ shopUserInfo: ShopUserInfo; // 用户信息
+}
+
+/** 订单费用汇总(修改:补充商家减免类型和明细) */
+export interface OrderCostSummary {
+ // 商品总件数
+ goodsTotal: number;
+ totalDiscountAmount: number;
+ goodsRealAmount: number; // 商品真实原价总和
+ goodsOriginalAmount: number; // 商品原价总和
+ goodsDiscountAmount: number; // 商品折扣金额
+ couponDeductionAmount: number; // 优惠券总抵扣
+ productCouponDeduction: number; // 商品优惠券抵扣
+ fullCouponDeduction: number; // 满减优惠券抵扣
+ pointDeductionAmount: number; // 积分抵扣金额
+ seatFee: number; // 餐位费
+ packFee: number; // 打包费
+ scoreMaxMoney: number; // 积分最大可抵扣金额
+ // 新增:商家减免明细
+ merchantReduction: {
+ type: MerchantReductionType; // 实际使用的减免类型
+ originalConfig: MerchantReductionConfig; // 原始配置(便于前端展示)
+ actualAmount: number; // 实际减免金额(计算后的值,≥0)
+ };
+ additionalFee: number; // 附加费
+ finalPayAmount: number; // 最终实付金额
+ couponUsed?: Coupon; // 实际使用的优惠券
+ pointUsed: number; // 实际使用的积分
+ newUserDiscount: number; // 新用户减免金额(元,默认0)
+ dinnerType?: "dine-in" | "take-out"; // 就餐类型(堂食/自取/配送/快递)
+ config: OrderExtraConfig; // 订单额外费用配置
+ //满减活动
+ fullReduction: {
+ usedFullReductionActivityFullAmount: number; // 计算出的满减活动的门槛金额
+ usedActivity?: FullReductionActivity; // 实际使用的满减活动
+ usedThreshold?: FullReductionThreshold; // 实际使用的满减阈值(多门槛中选最优)
+ actualAmount: number; // 满减实际减免金额(元)
+ };
+ vipDiscountAmount: number; //会员折扣减免金额
+ // 订单原支付金额
+ orderOriginFinalPayAmount: number; //订单原金额(包含打包费+餐位费)
+}
+
+/** 满减活动阈值(单条满减规则:满X减Y)- 对应 MkDiscountThresholdInsertGroupDefaultGroup */
+export interface FullReductionThreshold {
+ activityId?: number; // 关联满减活动ID
+ fullAmount?: number; // 满多少金额(元,必填)
+ discountAmount?: number; // 减多少金额(元,必填)
+}
+
+/** 满减活动主表 - 对应 Request 接口(后端真实字段) */
+export interface FullReductionActivity {
+ id?: number; // 自增主键(后端字段:id)
+ shopId?: number; // 店铺ID(后端字段:shopId)
+ status?: number; // 活动状态:1=未开始,2=进行中,3=已结束(后端字段:status)
+ sort?: number; // 排序值(越大优先级越高,后端字段:sort)
+ createTime?: string; // 创建时间(后端字段:createTime,格式如"2025-10-14 13:56:07")
+ updateTime?: string; // 最新修改时间(后端字段:updateTime,用于优先级排序)
+ validStartTime?: string; // 有效期开始时间(后端字段:validStartTime,格式如"2025-10-14")
+ validEndTime?: string; // 有效期结束时间(后端字段:validEndTime,格式如"2025-12-14")
+ useType?: string; // 可使用类型(后端字段:useType,如"dine,pickup,deliv,express")
+ useDays?: string; // 可用周期(后端字段:useDays,如"周一,周二,周三,周四,周五,周六,周日")
+ useTimeType?: string; // 可用时间段类型(后端字段:useTimeType,all=全时段,custom=指定时段)
+ useStartTime?: string; // 每日可用开始时间(后端字段:useStartTime,如"09:00:00",仅custom时有效)
+ useEndTime?: string; // 每日可用结束时间(后端字段:useEndTime,如"22:00:00",仅custom时有效)
+ couponShare?: number; // 与优惠券同享:0=否,1=是(后端字段:couponShare)
+ discountShare?: number; // 与限时折扣同享:0=否,1=是(后端字段:discountShare)
+ vipPriceShare?: number; // 与会员价同享:0=否,1=是(后端字段:vipPriceShare)
+ pointsShare?: number; // 与积分抵扣同享:0=否,1=是(后端字段:pointsShare)
+ thresholds?: FullReductionThreshold[]; // 满减阈值列表(多门槛,后端字段:thresholds)
+ isDel?: boolean; // 是否删除:0=否,1=是(后端字段:isDel,默认false)
+}
+
+// 辅助枚举:星期映射(用于useDays校验)
+const WEEKDAY_MAP = {
+ 周一: 1,
+ 周二: 2,
+ 周三: 3,
+ 周四: 4,
+ 周五: 5,
+ 周六: 6,
+ 周日: 0, // JS中getDay()返回0=周日
+};
+
+/**
+ * 辅助:校验当前时间是否在活动的「每日可用时段」内
+ * @param activity 满减活动
+ * @param currentTime 当前时间
+ * @returns 是否在时段内
+ */
+function isInDailyTimeRange(
+ activity: FullReductionActivity,
+ currentTime: Date
+): boolean {
+ // 全时段无需校验
+ if (activity.useTimeType === "all") return true;
+ // 无时段配置则不通过
+ if (!activity.useStartTime || !activity.useEndTime) return false;
+
+ const [startHour, startMinute] = activity.useStartTime.split(":").map(Number);
+ const [endHour, endMinute] = activity.useEndTime.split(":").map(Number);
+ const currentHour = currentTime.getHours();
+ const currentMinute = currentTime.getMinutes();
+
+ // 转换为分钟数比较
+ const startTotalMin = startHour * 60 + startMinute;
+ const endTotalMin = endHour * 60 + endMinute;
+ const currentTotalMin = currentHour * 60 + currentMinute;
+
+ // 处理跨天场景(如23:00-02:00)
+ if (startTotalMin <= endTotalMin) {
+ return currentTotalMin >= startTotalMin && currentTotalMin <= endTotalMin;
+ } else {
+ return currentTotalMin >= startTotalMin || currentTotalMin <= endTotalMin;
+ }
+}
+
+/**
+ * 辅助:校验当前时间是否在活动的「可用周期」内(如周一至周日)
+ * @param activity 满减活动
+ * @param currentTime 当前时间
+ * @returns 是否在周期内
+ */
+function isInWeeklyCycle(
+ activity: FullReductionActivity,
+ currentTime: Date
+): boolean {
+ // 无周期配置则不通过
+ if (!activity.useDays) return false;
+ const currentWeekday = currentTime.getDay(); // 0=周日,1=周一...6=周六
+ const allowedWeekdays = activity.useDays
+ .split(",")
+ .map((day) => WEEKDAY_MAP[day as keyof typeof WEEKDAY_MAP]);
+ return allowedWeekdays.includes(currentWeekday);
+}
+
+/**
+ * 辅助:校验当前就餐类型是否在活动的「可用类型」内(如堂食/自取)
+ * @param activity 满减活动
+ * @param currentDinnerType 当前就餐类型
+ * @returns 是否匹配
+ */
+function isDinnerTypeMatch(
+ activity: FullReductionActivity,
+ currentDinnerType: string
+): boolean {
+ if (!activity.useType) return false;
+ const allowedTypes = activity.useType.split(",");
+ //满减活动的就餐类型和当前券类型字段值不一样,暂时返回true
+ return true;
+}
+
+//判断商品是否可以使用限时折扣
+export function returnCanUseLimitTimeDiscount(
+ goods: BaseCartItem,
+ limitTimeDiscount: TimeLimitDiscountConfig | null | undefined,
+ useVipPrice: boolean,
+ idKey = "product_id"
+) {
+ if (!limitTimeDiscount || !limitTimeDiscount.id) {
+ return false;
+ }
+ const canUseFoods = (limitTimeDiscount.foods || "").split(",");
+ const goodsCanUse =
+ limitTimeDiscount.foodType == 1 || canUseFoods.includes("" + goods[idKey as keyof BaseCartItem]);
+ if (!goodsCanUse) {
+ return false;
+ }
+ if (limitTimeDiscount.discountPriority == "limit-time") {
+ return true;
+ }
+ if (limitTimeDiscount.discountPriority == "vip-price") {
+ if (!useVipPrice) {
+ return true;
+ }
+ if (useVipPrice && goods.hasOwnProperty("memberPrice")) {
+ if (goods.memberPrice && goods.memberPrice * 1 <= 0) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function returnMemberPrice(useVipPrice: boolean, goods: BaseCartItem) {
+ if (useVipPrice) {
+ return goods.memberPrice || goods.salePrice;
+ } else {
+ return goods.salePrice;
+ }
+}
+
+/**
+ * 返回商品限时折扣价格
+ */
+function returnLimitPrice(
+ goods: BaseCartItem,
+ limitTimeDiscount: TimeLimitDiscountConfig | null | undefined,
+ useVipPrice: boolean
+) {
+ if (!limitTimeDiscount) {
+ return 0;
+ }
+ const discountRate = new BigNumber(limitTimeDiscount.discountRate).dividedBy(
+ 100
+ );
+
+ const canuseLimit = returnCanUseLimitTimeDiscount(
+ goods,
+ limitTimeDiscount,
+ useVipPrice
+ );
+ if (canuseLimit) {
+ //可以使用限时折扣
+ if (limitTimeDiscount.discountPriority == "limit-time") {
+ //限时价优先
+ const result = BigNumber(goods.salePrice)
+ .times(discountRate)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ return result;
+ }
+ if (limitTimeDiscount.discountPriority == "vip-price") {
+ //会员价优先
+ if (useVipPrice && goods.memberPrice && goods.memberPrice * 1 > 0) {
+ //使用会员价
+ return returnMemberPrice(useVipPrice, goods);
+ } else {
+ //不使用会员价
+ const result = BigNumber(goods.salePrice)
+ .times(discountRate)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ return result;
+ }
+ }
+ } else {
+ //不可以使用限时折扣
+ //会员价优先
+ if (useVipPrice) {
+ //使用会员价
+ return returnMemberPrice(useVipPrice, goods);
+ } else {
+ return goods.salePrice;
+ }
+ }
+}
+
+/**
+ * 计算商品计算门槛时的金额
+ */
+
+export function returnCalcPrice(
+ goods: BaseCartItem,
+ fullReductionActivitie: FullReductionActivity | undefined,
+ limitTimeDiscount: TimeLimitDiscountConfig | null | undefined,
+ useVipPrice: boolean,
+ idKey = "product_id"
+) {
+ if (goods.discountSaleAmount && goods.discountSaleAmount * 1 > 0) {
+ return goods.salePrice;
+ }
+ //限时折扣和满减活动都有
+ if (fullReductionActivitie && limitTimeDiscount) {
+ if (
+ fullReductionActivitie.discountShare == 1 &&
+ fullReductionActivitie.vipPriceShare == 1
+ ) {
+ //与限时折扣同享,与会员价不同享
+ return returnLimitPrice(goods, limitTimeDiscount, useVipPrice);
+ }
+ if (
+ fullReductionActivitie.discountShare != 1 &&
+ fullReductionActivitie.vipPriceShare == 1
+ ) {
+ //与限时折扣不同享,与会员价同享
+ return returnMemberPrice(useVipPrice, goods);
+ }
+ if (fullReductionActivitie.vipPriceShare != 1) {
+ //与会员价不同享
+ return goods.salePrice;
+ }
+ return goods.salePrice;
+ }
+ //只有满减活动
+ if (fullReductionActivitie) {
+ if (fullReductionActivitie.vipPriceShare == 1) {
+ return returnMemberPrice(useVipPrice, goods);
+ } else {
+ return goods.salePrice;
+ }
+ }
+ //只有限时折扣
+ if (limitTimeDiscount) {
+ return returnLimitPrice(goods, limitTimeDiscount, useVipPrice);
+ }
+
+ if (useVipPrice) {
+ return returnMemberPrice(useVipPrice, goods);
+ }
+ return goods.salePrice;
+}
+
+/**
+ * 计算满减活动门槛
+ */
+export function calcFullReductionActivityFullAmount(
+ goodsList: BaseCartItem[],
+ fullReductionActivitie: FullReductionActivity | undefined,
+ limitTimeDiscount: TimeLimitDiscountConfig | null | undefined,
+ useVipPrice: boolean,
+ seatFee: number,
+ packFee: number
+): number {
+ if (!fullReductionActivitie) {
+ return 0;
+ }
+ let amount = 0;
+ for (let goods of goodsList) {
+ const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+ if (goods.is_temporary || goods.is_gift || availableNum <= 0) {
+ //临时菜,赠菜,数量<=0的商品不计算
+ continue;
+ }
+ const calcPrice = returnCalcPrice(
+ goods,
+ fullReductionActivitie,
+ limitTimeDiscount,
+ useVipPrice,
+ "product_id"
+ );
+ if (calcPrice !== undefined) {
+ amount += calcPrice * availableNum;
+ }
+ }
+ return amount + seatFee + packFee;
+ console.log("amount", amount);
+}
+
+/**
+ * 筛选最优满减活动(对齐后端逻辑:状态→时间→周期→时段→就餐类型→排序→修改时间)
+ * @param activities 后端返回的满减活动列表
+ * @param currentShopId 当前店铺ID
+ * @param currentDinnerType 当前就餐类型(dine/pickup等)
+ * @param currentTime 当前时间(默认当前时间)
+ * @returns 最优满减活动(无符合条件则返回undefined)
+ */
+export function filterOptimalFullReductionActivity(
+ activities: FullReductionActivity[],
+ currentShopId: number,
+ currentDinnerType: string,
+ currentTime: Date = new Date()
+): FullReductionActivity | undefined {
+ if (!activities || !activities.length) return undefined;
+ // 第一步:基础筛选(未删除+当前店铺+活动进行中+就餐类型匹配)
+ const baseEligible = activities.filter((activity) => {
+ return (
+ activity.isDel !== true && // 未删除
+ // activity.shopId === currentShopId && // 当前店铺
+ // activity.status === 2 && // 状态=2(进行中)
+ isDinnerTypeMatch(activity, currentDinnerType) && // 就餐类型匹配
+ activity.thresholds?.length // 至少有一个满减阈值
+ );
+ });
+ console.log("baseEligible", baseEligible);
+
+ if (!baseEligible.length) return undefined;
+
+ // 第二步:时间筛选(有效期内+周期内+时段内)
+ const timeEligible = baseEligible.filter((activity) => {
+ // 1. 校验有效期(validStartTime ~ validEndTime)
+ if (activity.useTimeType == "all") {
+ return true;
+ }
+ if (!activity.validStartTime || !activity.validEndTime) return false;
+ const startDate = new Date(activity.validStartTime);
+ const endDate = new Date(activity.validEndTime);
+ // 处理有效期结束日期的23:59:59
+ endDate.setHours(23, 59, 59, 999);
+ if (currentTime < startDate || currentTime > endDate) return false;
+
+ // 2. 校验可用周期(如周一至周日)
+ if (!isInWeeklyCycle(activity, currentTime)) return false;
+
+ // 3. 校验每日可用时段(如09:00-22:00)
+ if (!isInDailyTimeRange(activity, currentTime)) return false;
+
+ return true;
+ });
+
+ if (!timeEligible.length) return undefined;
+
+ // 第三步:按优先级排序(需求规则)
+ return timeEligible.sort((a, b) => {
+ // 1. 先比排序值:排序值大的优先
+ if ((a.sort || 0) !== (b.sort || 0)) {
+ return (b.sort || 0) - (a.sort || 0);
+ }
+ // 2. 再比修改时间:最新修改的优先(时间戳降序)
+ const aUpdateTime = a.updateTime ? new Date(a.updateTime).getTime() : 0;
+ const bUpdateTime = b.updateTime ? new Date(b.updateTime).getTime() : 0;
+ return bUpdateTime - aUpdateTime;
+ })[0]; // 取最优活动
+}
+/**
+ * 折扣率格式化:后端discountRate(%)→ 工具库小数(如90→0.9)
+ */
+export function formatDiscountRate(backendDiscountRate?: number): number {
+ if (!backendDiscountRate || backendDiscountRate <= 0) return 1; // 默认无折扣(1=100%)
+ // 后端若为百分比(如90=9折),除以100;若已为小数(如0.9)直接返回
+ return backendDiscountRate >= 1
+ ? backendDiscountRate / 100
+ : backendDiscountRate;
+}
+
+/**
+ * 统一小数处理:舍去小数点后两位以后的数字(解决浮点数精度偏差)
+ * 如 10.129 → 10.12,1.01 → 1.01,1.0100000001 → 1.01
+ * @param num 待处理数字
+ * @returns 处理后保留两位小数的数字
+ */
+export function truncateToTwoDecimals(num: number | string): number {
+ return new BigNumber(num).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber();
+}
+
+/**
+ * 判断商品是否为临时菜(临时菜不参与优惠券门槛和折扣计算)
+ * @param goods 商品项
+ * @returns 是否临时菜
+ */
+export function isTemporaryGoods(goods: BaseCartItem): boolean {
+ return !!goods.is_temporary;
+}
+
+/**
+ * 判断商品是否为赠菜(赠菜不计入优惠券活动,但计打包费)
+ * @param goods 商品项
+ * @returns 是否赠菜
+ */
+export function isGiftGoods(goods: BaseCartItem): boolean {
+ return !!goods.is_gift;
+}
+/**
+ * 判断可用类型是否可用
+ */
+export function useTypeCanUse(useType: string[]) {
+ const arr = ["all", "dine-in", "take-out", "take-away", "post"];
+ return useType.some((item) => arr.includes(item));
+}
+
+/**
+ * 计算单个商品的会员价(优先级:SKU会员价 > 商品会员价 > 会员折扣率)
+ * @param goods 商品项
+ * @param isMember 是否会员
+ * @param memberDiscountRate 会员折扣率(如0.95=95折)
+ * @returns 会员价(元)
+ */
+export function calcMemberPrice(
+ goods: BaseCartItem,
+ isMember: boolean,
+ memberDiscountRate?: number
+): number {
+ if (!isMember) return truncateToTwoDecimals(goods.salePrice);
+
+ // 优先级:SKU会员价 > 商品会员价 > 商品原价(无会员价时用会员折扣)
+ const basePrice = goods.memberPrice || goods.salePrice;
+ // 仅当无SKU会员价、无商品会员价时,才应用会员折扣率
+ if (memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice) {
+ return truncateToTwoDecimals(
+ new BigNumber(basePrice).multipliedBy(memberDiscountRate).toNumber()
+ );
+ }
+
+ return truncateToTwoDecimals(basePrice);
+}
+
+/**
+ * 过滤可参与优惠券计算的商品(排除临时菜、赠菜、已用兑换券的商品)
+ * @param goodsList 商品列表
+ * @param excludedProductIds 需排除的商品ID列表(商品ID,非购物车ID)
+ * @returns 可参与优惠券计算的商品列表
+ */
+export function filterCouponEligibleGoods(
+ goodsList: BaseCartItem[],
+ excludedProductIds: string[] = []
+): BaseCartItem[] {
+ return goodsList.filter(
+ (goods) =>
+ !isTemporaryGoods(goods) &&
+ !isGiftGoods(goods) &&
+ !excludedProductIds.includes(String(goods.product_id)) // 核心修正:用商品ID排除
+ );
+}
+
+/**
+ * 统一筛选门槛商品的工具函数(所有券类型复用,按商品ID匹配)
+ * @param baseEligibleGoods 基础合格商品(已排除临时菜/赠菜/已抵扣商品)
+ * @param applicableProductIds 优惠券指定的门槛商品ID数组
+ * @returns 最终参与优惠券计算的商品列表
+ */
+export function filterThresholdGoods(
+ baseEligibleGoods: BaseCartItem[],
+ applicableProductIds: string[]
+): BaseCartItem[] {
+ // 空数组=全部基础合格商品;非空=仅商品ID匹配的商品(转字符串兼容类型)
+ return applicableProductIds.length === 0
+ ? baseEligibleGoods
+ : baseEligibleGoods.filter((goods) =>
+ applicableProductIds.includes(String(goods.product_id))
+ ); // 核心修正:用商品ID匹配
+}
+
+/**
+ * 商品排序(用于商品兑换券:按价格/数量/加入顺序排序,按商品ID分组去重)
+ * @param goodsList 商品列表
+ * @param sortRule 排序规则(low_price_first/high_price_first)
+ * @param cartOrder 商品加入购物车的顺序(key=购物车ID,value=加入时间戳)
+ * @returns 排序后的商品列表
+ */
+export function sortGoodsForCoupon(
+ goodsList: BaseCartItem[],
+ sortRule: "low_price_first" | "high_price_first",
+ cartOrder: Record = {}
+): BaseCartItem[] {
+ return [...goodsList].sort((a, b) => {
+ // 1. 按商品单价排序(优先级最高)
+ const priceA = a.skuData?.salePrice ?? a.salePrice;
+ const priceB = b.skuData?.salePrice ?? b.salePrice;
+ if (priceA !== priceB) {
+ return sortRule === "low_price_first" ? priceA - priceB : priceB - priceA;
+ }
+
+ // 2. 同价格按商品数量排序(降序,多的优先)
+ if (a.number !== b.number) {
+ return b.number - a.number;
+ }
+
+ // 3. 同价格同数量按加入购物车顺序(早的优先,用购物车ID匹配)
+ const orderA = cartOrder[String(a.id)] ?? Infinity;
+ const orderB = cartOrder[String(b.id)] ?? Infinity;
+ return orderA - orderB;
+ });
+}
+
+/**
+ * 计算优惠券门槛金额(根据同享规则,按商品ID匹配限时折扣)
+ * @param eligibleGoods 可参与优惠券的商品列表(已过滤临时菜/赠菜)
+ * @param coupon 优惠券(含discountShare/vipPriceShare配置)
+ * @param config 订单配置(会员信息)
+ * @param activities 全局营销活动(限时折扣,applicableProductIds为商品ID)
+ * @returns 满足优惠券门槛的金额基数
+ */
+export function calcCouponThresholdAmount(
+ eligibleGoods: BaseCartItem[],
+ coupon: BaseCoupon,
+ config: Pick,
+ activities: ActivityConfig[] = []
+): number {
+ let total = new BigNumber(0);
+
+ for (const goods of eligibleGoods) {
+ const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+ if (availableNum <= 0) continue;
+
+ // 1. 基础金额:默认用商品原价(SKU原价优先)
+ const basePrice = new BigNumber(
+ goods.skuData?.salePrice ?? goods.salePrice
+ );
+ let itemAmount = basePrice.multipliedBy(availableNum);
+
+ // 2. 处理「与会员价/会员折扣同享」规则:若开启,用会员价计算
+ if (coupon.vipPriceShare) {
+ const memberPrice = new BigNumber(
+ calcMemberPrice(goods, config.isMember, config.memberDiscountRate)
+ );
+ itemAmount = memberPrice.multipliedBy(availableNum);
+ }
+
+ // 3. 处理「与限时折扣同享」规则:若开启,叠加限时折扣(按商品ID匹配活动)
+ if (coupon.discountShare) {
+ const activity =
+ goods.activityInfo ??
+ activities.find(
+ (act) =>
+ act.type === ActivityType.TIME_LIMIT_DISCOUNT &&
+ (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动
+ );
+
+ if (activity) {
+ itemAmount = itemAmount.multipliedBy(activity.discountRate); // 叠加限时折扣
+ }
+ }
+
+ total = total.plus(itemAmount);
+ }
+
+ return truncateToTwoDecimals(total.toNumber());
+}
+
+// ============================ 3. 商品核心价格计算(核心修正:按商品ID匹配营销活动) ============================
+/**
+ * 计算单个商品的实际单价(整合商家改价、会员优惠、营销活动折扣,按商品ID匹配活动)
+ * @param goods 商品项
+ * @param config 订单额外配置(含会员、活动信息)
+ * @returns 单个商品实际单价(元)
+ */
+export function calcSingleGoodsRealPrice(
+ goods: BaseCartItem,
+ config: Pick<
+ OrderExtraConfig,
+ "isMember" | "memberDiscountRate" | "limitTimeDiscount"
+ >
+): number {
+ const { isMember, memberDiscountRate, limitTimeDiscount: activity } = config;
+ console.log("activity", activity);
+
+ //如果是增菜价格为0
+ if (goods.is_gift) {
+ return 0;
+ }
+
+ // 1. 优先级1:商家改价(改价后单价>0才生效)
+ if (goods.discountSaleAmount && goods.discountSaleAmount > 0) {
+ return truncateToTwoDecimals(goods.discountSaleAmount);
+ }
+
+ // 2. 优先级2:会员价(含会员折扣率,SKU会员价优先)
+ const memberPrice = new BigNumber(
+ calcMemberPrice(goods, isMember, memberDiscountRate)
+ );
+
+ // 3. 优先级3:营销活动折扣(如限时折扣,需按商品ID匹配活动)
+ let isActivityApplicable = false;
+ if (activity) {
+ if (activity.foodType == 1) {
+ isActivityApplicable = true;
+ } else {
+ const canUseGoods = activity.foods?.split(",") || [];
+ if (canUseGoods.find((v) => v == String(goods.product_id))) {
+ isActivityApplicable = true;
+ }
+ }
+ }
+ if (!activity || !isActivityApplicable) {
+ return memberPrice.toNumber();
+ }
+ console.log("isMember", isMember);
+ //限时折扣优先或者会员价优先但是不是会员或者未开启会员价格时限时折扣优先
+ if (
+ activity.discountPriority == "limit-time" ||
+ (activity.discountPriority == "vip-price" && !isMember) ||
+ (activity.discountPriority == "vip-price" && isMember && !goods.memberPrice)
+ ) {
+ //限时折扣优先
+ return truncateToTwoDecimals(
+ new BigNumber(goods.salePrice)
+ .times(activity.discountRate / 100)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber()
+ );
+ }
+ if (activity.discountPriority == "vip-price" && isMember) {
+ return memberPrice.toNumber();
+ }
+
+ // 处理活动与会员的同享/不同享逻辑
+ if (activity.vipPriceShare) {
+ // 同享:会员价基础上叠加工活动折扣
+ return truncateToTwoDecimals(
+ memberPrice.multipliedBy(activity.discountRate).toNumber()
+ );
+ } else {
+ // 不同享:取会员价和活动价的最小值(活动价用SKU原价计算)
+ const basePriceForActivity = new BigNumber(
+ goods.skuData?.salePrice ?? goods.salePrice
+ );
+ const activityPrice = basePriceForActivity.multipliedBy(
+ activity.discountRate
+ );
+
+ return truncateToTwoDecimals(
+ memberPrice.isLessThanOrEqualTo(activityPrice)
+ ? memberPrice.toNumber()
+ : activityPrice.toNumber()
+ );
+ }
+}
+
+/**
+ * 计算商品原价总和(所有商品:原价*数量,含临时菜、赠菜,用SKU原价优先)
+ * @param goodsList 商品列表
+ * @returns 商品原价总和(元)
+ */
+export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number {
+ let total = new BigNumber(0);
+
+ for (const goods of goodsList) {
+ const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+ let basePrice = new BigNumber(0);
+ if (goods.is_temporary) {
+ basePrice = new BigNumber(goods?.discountSaleAmount ?? 0);
+ } else if (goods.is_gift) {
+ basePrice = new BigNumber(0);
+ } else {
+ basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); // SKU原价优先
+ }
+
+ total = total.plus(basePrice.multipliedBy(availableNum));
+ }
+
+ return truncateToTwoDecimals(total.toNumber());
+}
+
+/**
+ * 计算商品实际总价(所有商品:实际单价*数量,含临时菜、赠菜,按商品ID匹配活动)
+ * @param goodsList 商品列表
+ * @param config 订单额外配置(含会员、活动信息)
+ * @param activities 全局营销活动列表(如限时折扣,applicableProductIds为商品ID)
+ * @returns 商品实际总价(元)
+ */
+export function calcGoodsRealAmount(
+ goodsList: BaseCartItem[],
+ config: Pick<
+ OrderExtraConfig,
+ "isMember" | "memberDiscountRate" | "limitTimeDiscount"
+ >
+): number {
+ let total = new BigNumber(0);
+
+ for (const goods of goodsList) {
+ const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+ if (availableNum <= 0) continue;
+ console.log("goods", goods);
+ const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, config));
+ total = total.plus(realPrice.multipliedBy(availableNum));
+ }
+
+ return truncateToTwoDecimals(total.toNumber());
+}
+
+/**
+ * 计算商品折扣总金额(商品原价总和 - 商品实际总价)
+ * @param goodsOriginalAmount 商品原价总和
+ * @param goodsRealAmount 商品实际总价
+ * @returns 商品折扣总金额(元,≥0)
+ */
+export function calcGoodsDiscountAmount(
+ goodsOriginalAmount: number,
+ goodsRealAmount: number
+): number {
+ const original = new BigNumber(goodsOriginalAmount);
+ const real = new BigNumber(goodsRealAmount);
+ const discount = original.minus(real);
+
+ return truncateToTwoDecimals(Math.max(0, discount.toNumber()));
+}
+/**
+ * 从满减活动的多门槛中,选择最优阈值(满金额最小且减免金额最大)
+ * @param thresholds 满减阈值列表(多门槛)
+ * @param baseAmount 满减计算基数(新客立减后的金额)
+ * @param goodsOriginalAmount 商品原价总和(discountShare=0时用)
+ * @param goodsRealAmount 商品折扣后总和(discountShare=1时用)
+ * @param discountShare 与限时折扣同享:0=否,1=是
+ * @returns 最优阈值(无符合条件则返回undefined)
+ */
+export function selectOptimalThreshold(
+ thresholds: FullReductionThreshold[] = [],
+ baseAmount: number,
+ goodsOriginalAmount: number,
+ goodsRealAmount: number,
+ discountShare: number = 0
+): FullReductionThreshold | undefined {
+ if (!thresholds.length) return undefined;
+
+ // 第一步:确定满减门槛基数(根据discountShare规则)
+ const thresholdBase = baseAmount;
+
+ // 第二步:筛选「满金额≤基数」且「减免金额>0」的有效阈值
+ const validThresholds = thresholds.filter((threshold) => {
+ const fullAmount = new BigNumber(threshold.fullAmount || 0);
+ const discountAmount = new BigNumber(threshold.discountAmount || 0);
+ console.log("fullAmount", fullAmount);
+ console.log("discountAmount", discountAmount);
+
+ return (
+ fullAmount.isLessThanOrEqualTo(thresholdBase) &&
+ discountAmount.isGreaterThan(0)
+ );
+ });
+ console.log("validThresholds", validThresholds);
+
+ if (!validThresholds.length) return undefined;
+
+ // 找到抵扣金额最大的门槛项
+ const maxDiscountThreshold = validThresholds.reduce(
+ (maxItem, currentItem) => {
+ // 处理空值,默认抵扣金额为0
+ const maxDiscount = new BigNumber(maxItem?.discountAmount || 0);
+ const currentDiscount = new BigNumber(currentItem?.discountAmount || 0);
+
+ // 比较当前项和已存最大项的抵扣金额,保留更大的
+ return currentDiscount.gt(maxDiscount) ? currentItem : maxItem;
+ },
+ validThresholds[0] || null
+ ); // 初始值为数组第一项(若数组为空则返回null)
+ console.log("maxDiscountThreshold", maxDiscountThreshold);
+ return maxDiscountThreshold;
+}
+
+/**
+ * 计算满减实际减免金额(适配多门槛、同享规则)
+ * @param optimalActivity 最优满减活动
+ * @param optimalThreshold 最优满减阈值
+ * @param baseAmount 计算基数(新客立减后的金额)
+ * @returns 实际减免金额(元,未达门槛则0)
+ */
+export function calcFullReductionAmount(
+ baseAmount: number,
+ optimalActivity?: FullReductionActivity,
+ optimalThreshold?: FullReductionThreshold
+): number {
+ if (!optimalActivity || !optimalThreshold) return 0;
+
+ const baseAmountBn = new BigNumber(baseAmount);
+ const discountAmountBn = new BigNumber(optimalThreshold.discountAmount || 0);
+
+ // 1. 基数必须为正(避免减免后为负)
+ if (baseAmountBn.isLessThanOrEqualTo(0)) return 0;
+
+ // 2. 减免金额不能超过基数(避免减成负数)
+ const maxReducible = baseAmountBn;
+ const actualReduction = discountAmountBn.isLessThanOrEqualTo(maxReducible)
+ ? discountAmountBn
+ : maxReducible;
+
+ return truncateToTwoDecimals(actualReduction.toNumber());
+}
+
+// ------------------------------ 策略辅助函数 ------------------------------
+/**
+ * 根据优惠券useShops列表判断门店是否匹配(适配BaseCoupon的useShops字段)
+ */
+function isStoreMatchByList(
+ useShops: string[],
+ currentStoreId: string
+): boolean {
+ // 适用门店为空数组 → 无限制(所有门店适用)
+ if (useShops.length === 0) return true;
+ // 匹配当前门店ID(字符串比较,避免类型问题)
+ return useShops.includes(currentStoreId);
+}
+
+/**
+ * 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除,新增细分统计)
+ * @param backendCoupons 后端优惠券列表
+ * @param goodsList 商品列表
+ * @param config 订单配置(含就餐类型)
+ * @returns 最优优惠券的抵扣结果(含商品券/满减券细分)
+ */
+export function calcCouponDeduction(
+ backendCoupons: BackendCoupon[],
+ goodsList: BaseCartItem[],
+ config: Pick<
+ OrderExtraConfig,
+ "currentStoreId" | "isMember" | "memberDiscountRate"
+ > & {
+ activities: ActivityConfig[];
+ cartOrder: Record;
+ dinnerType: "dine-in" | "take-out";
+ currentTime?: Date;
+ }
+): {
+ deductionAmount: number;
+ productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券/折扣券/买一送一等)
+ fullCouponDeduction: number; // 新增:满减优惠券抵扣
+ usedCoupon?: Coupon;
+ excludedProductIds: string[]; // 排除的商品ID列表(商品ID)
+} {
+ const goodsCoupon = backendCoupons.filter((v) => v.type == 2);
+ const discountCoupon = backendCoupons.filter((v) => v.type != 2);
+
+ // 3. 计算非兑换券最优抵扣(传递已抵扣商品ID,避免重复,统计细分字段)
+ let nonExchangeResult: CouponResult = {
+ deductionAmount: discountCoupon.reduce((prve, cur): number => {
+ return prve + (cur.discountAmount || 0);
+ }, 0),
+ excludedProductIds: [],
+ usedCoupon: undefined,
+ productCouponDeduction: 0,
+ fullCouponDeduction: 0,
+ };
+
+ // 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID,统计商品券细分)
+ let exchangeResult: ExchangeCalculationResult = {
+ deductionAmount: goodsCoupon.reduce((prve, cur): number => {
+ return prve + (cur.discountAmount || 0);
+ }, 0),
+ excludedProductIds: [],
+ productCouponDeduction: 0,
+ };
+
+ // 5. 汇总结果:兑换券与非兑换券不可同时使用,取抵扣金额大的
+ const exchangeBn = new BigNumber(exchangeResult.deductionAmount);
+ const nonExchangeBn = new BigNumber(nonExchangeResult.deductionAmount);
+ const isExchangeBetter = exchangeBn.isGreaterThan(nonExchangeBn);
+
+ return {
+ deductionAmount: exchangeBn.plus(nonExchangeBn).toNumber(),
+ productCouponDeduction: exchangeResult.deductionAmount,
+ fullCouponDeduction: nonExchangeResult.deductionAmount, // 兑换券与满减券互斥,满减券抵扣置0
+ usedCoupon: isExchangeBetter ? undefined : nonExchangeResult.usedCoupon,
+ excludedProductIds: isExchangeBetter
+ ? exchangeResult.excludedProductIds
+ : nonExchangeResult.excludedProductIds,
+ };
+}
+
+// ============================ 5. 其他费用计算(无ID依赖,逻辑不变) ============================
+/**
+ * 计算总打包费(赠菜也计算,称重商品打包数量≤1)
+ * @param goodsList 商品列表
+ * @param dinnerType 就餐类型(堂食dine-in/外卖take-out)
+ * @returns 总打包费(元)
+ */
+export function calcTotalPackFee(
+ goodsList: BaseCartItem[],
+ dinnerType: "dine-in" | "take-out"
+): number {
+ // if (dinnerType !== "take-out") return 0;
+ let total = new BigNumber(0);
+
+ for (const goods of goodsList) {
+ const packNumber = goods.packNumber ? goods.packNumber * 1 : 0;
+ let availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
+
+ if (availableNum === 0) continue;
+
+ // 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1)
+ let packNum = Math.min(availableNum, packNumber);
+ if (dinnerType === "take-out") {
+ packNum = availableNum;
+ }
+ if (goods.product_type === GoodsType.WEIGHT) {
+ packNum = Math.min(packNum, 1);
+ }
+
+ total = total.plus(new BigNumber(goods.packFee || 0).multipliedBy(packNum));
+ }
+
+ return truncateToTwoDecimals(total.toNumber());
+}
+
+/**
+ * 计算餐位费(按人数,不参与营销活动)
+ * @param config 餐位费配置
+ * @returns 餐位费(元,未启用则0)
+ */
+export function calcSeatFee(config: SeatFeeConfig): number {
+ if (!config.isEnabled || config.personCount == 0) return 0;
+ const personCount = Math.max(1, config.personCount); // 至少1人
+ return truncateToTwoDecimals(
+ new BigNumber(config.pricePerPerson).multipliedBy(personCount).toNumber()
+ );
+}
+
+/**
+ * 计算积分抵扣金额(按X积分=1元,不超过最大抵扣和用户积分)
+ * @param userPoints 用户当前积分
+ * @param rule 积分抵扣规则
+ * @param maxDeductionLimit 最大抵扣上限(通常为订单金额,避免超扣)
+ * @returns 积分抵扣金额 + 实际使用积分
+ */
+export function calcPointDeduction(
+ userPoints: number,
+ rule: PointDeductionRule,
+ maxDeductionLimit: number
+): {
+ deductionAmount: number;
+ usedPoints: number;
+} {
+ console.log("calcPointDeduction", userPoints, rule, maxDeductionLimit);
+ if (rule.pointsPerYuan <= 0 || userPoints <= 0) {
+ return { deductionAmount: 0, usedPoints: 0 };
+ }
+
+ const userPointsBn = new BigNumber(userPoints);
+ const pointsPerYuanBn = new BigNumber(rule.pointsPerYuan);
+ const maxLimitBn = new BigNumber(maxDeductionLimit);
+
+ // 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限)
+ const maxDeductByPoints = userPointsBn.dividedBy(pointsPerYuanBn);
+ const maxDeductAmount = maxDeductByPoints.isLessThan(
+ rule.maxDeductionAmount ?? Infinity
+ )
+ ? maxDeductByPoints
+ : new BigNumber(rule.maxDeductionAmount || Infinity).isLessThan(maxLimitBn)
+ ? maxDeductByPoints
+ : maxLimitBn;
+
+ // 实际使用积分 = 抵扣金额 * 积分兑换比例
+ const usedPoints = maxDeductAmount.multipliedBy(pointsPerYuanBn);
+
+ return {
+ deductionAmount: truncateToTwoDecimals(maxDeductAmount.toNumber()),
+ usedPoints: truncateToTwoDecimals(
+ Math.min(usedPoints.toNumber(), userPoints)
+ ), // 避免积分超扣
+ };
+}
+
+function calcVipDiscountAmount(
+ goodsRealAmount: number,
+ shopUserInfo: ShopUserInfo
+): number {
+ if (!shopUserInfo.isVip || shopUserInfo.discount === 0) return 0;
+ if (shopUserInfo.isVip == 1 && shopUserInfo.isMemberPrice != 1) {
+ return 0;
+ }
+ console.log("goodsRealAmount", goodsRealAmount);
+ console.log("discount", (100 - (shopUserInfo.discount || 0)) / 100);
+ return truncateToTwoDecimals(
+ new BigNumber(goodsRealAmount)
+ .times((100 - (shopUserInfo.discount || 0)) / 100)
+ .decimalPlaces(2, BigNumber.ROUND_DOWN)
+ .toNumber()
+ );
+}
+
+// ============================ 6. 订单总费用汇总与实付金额计算(核心入口,补充细分字段) ============================
+/**
+ * 计算订单所有费用子项并汇总(核心入口函数)
+ * @param goodsList 购物车商品列表
+ * @param dinnerType 就餐类型
+ * @param backendCoupons 后端优惠券列表
+ * @param activities 全局营销活动列表
+ * @param config 订单额外配置(会员、积分、餐位费等)
+ * @param cartOrder 商品加入购物车顺序(key=购物车ID,value=时间戳)
+ * @param currentTime 当前时间(用于优惠券有效期判断)
+ * @returns 订单费用汇总(含所有子项和实付金额,新增商品券/满减券细分)
+ */
+export function calculateOrderCostSummary(
+ goodsList: BaseCartItem[],
+ dinnerType: "dine-in" | "take-out", // 前端就餐类型
+ backendCoupons: BackendCoupon[] = [],
+ activities: ActivityConfig[] = [],
+ config: OrderExtraConfig, // 含后端满减活动、currentDinnerType
+ cartOrder: Record = {},
+ currentTime: Date = new Date()
+): OrderCostSummary {
+ //是否使用霸王餐,霸王餐配置
+ const {
+ isFreeDine,
+ freeDineConfig,
+ limitTimeDiscount,
+ fullReductionActivities,
+ shopUserInfo,
+ } = config;
+ console.log("shopUserInfo", shopUserInfo);
+
+ // ------------------------------ 1. 基础费用计算 ------------------------------
+ const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); // 商品原价总和
+ const goodsRealAmount = calcGoodsRealAmount(
+ // 商品折扣后总和
+ goodsList,
+ {
+ isMember: config.isMember,
+ memberDiscountRate: config.memberDiscountRate,
+ limitTimeDiscount: config.limitTimeDiscount,
+ }
+ );
+ const goodsDiscountAmount = calcGoodsDiscountAmount(
+ goodsOriginalAmount,
+ goodsRealAmount
+ ); // 商品折扣金额
+
+ const newUserDiscount = config.newUserDiscount || 0; // 新客立减
+
+ // 其他费用计算(原有逻辑不变) ------------------------------
+ const packFee = calcTotalPackFee(goodsList, dinnerType); // 打包费
+ let seatFee = calcSeatFee(config.seatFeeConfig); // 餐位费
+ seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费
+ const additionalFee = Math.max(0, config.additionalFee); // 附加费
+
+ // ------------------------------ 2. 满减活动计算(核心步骤) ------------------------------
+ let usedFullReductionActivity: FullReductionActivity | undefined;
+ let usedFullReductionThreshold: FullReductionThreshold | undefined;
+ let fullReductionAmount = 0;
+ let usedFullReductionActivityFullAmount = calcFullReductionActivityFullAmount(
+ goodsList,
+ usedFullReductionActivity,
+ config.limitTimeDiscount,
+ config.isMember,
+ seatFee,
+ packFee
+ );
+
+ // 2.1 筛选最优满减活动(后端活动列表、当前店铺、就餐类型、时间)
+ usedFullReductionActivity = filterOptimalFullReductionActivity(
+ config.fullReductionActivities,
+ Number(config.currentStoreId), // 转换为数字(后端shopId是number)
+ config.currentDinnerType, // 后端useType匹配的就餐类型(如"dine")
+ currentTime
+ );
+
+ // 2.2 计算满减基数(先扣新客立减)
+ let baseAfterNewUserDiscount = new BigNumber(
+ limitTimeDiscount &&
+ limitTimeDiscount.id &&
+ usedFullReductionActivity &&
+ !usedFullReductionActivity.discountShare
+ ? goodsRealAmount
+ : goodsRealAmount
+ )
+ .minus(newUserDiscount)
+ .plus(packFee)
+ .plus(seatFee)
+ .plus(additionalFee)
+ .toNumber();
+ baseAfterNewUserDiscount =
+ baseAfterNewUserDiscount > 0 ? baseAfterNewUserDiscount : 0;
+
+ // 2.3 选择最优满减阈值(多门槛场景)
+ if (usedFullReductionActivity) {
+ //计算当前满减活动的门槛金额
+ usedFullReductionActivityFullAmount = calcFullReductionActivityFullAmount(
+ goodsList,
+ usedFullReductionActivity,
+ config.limitTimeDiscount,
+ config.isMember,
+ seatFee,
+ packFee
+ );
+
+ usedFullReductionThreshold = selectOptimalThreshold(
+ usedFullReductionActivity.thresholds,
+ usedFullReductionActivityFullAmount,
+ goodsOriginalAmount,
+ goodsRealAmount,
+ usedFullReductionActivity.discountShare || 0 // 与限时折扣同享规则
+ );
+
+ // 2.4 计算满减实际减免金额
+ fullReductionAmount = calcFullReductionAmount(
+ baseAfterNewUserDiscount,
+ usedFullReductionActivity,
+ usedFullReductionThreshold
+ );
+ }
+
+ // ------------------------------ 3. 优惠券抵扣(适配满减同享规则) ------------------------------
+ let couponDeductionAmount = 0;
+ let productCouponDeduction = 0;
+ let fullCouponDeduction = 0;
+ let usedCoupon: Coupon | undefined;
+ let excludedProductIds: string[] = [];
+
+ const couponResult = calcCouponDeduction(
+ // 原有优惠券计算函数
+ backendCoupons,
+ goodsList,
+ {
+ currentStoreId: config.currentStoreId,
+ isMember: config.isMember,
+ memberDiscountRate: config.memberDiscountRate,
+ activities,
+ cartOrder,
+ dinnerType,
+ currentTime,
+ }
+ );
+ console.log("couponResult", couponResult);
+ couponDeductionAmount = couponResult.deductionAmount;
+ productCouponDeduction = couponResult.productCouponDeduction;
+ fullCouponDeduction = couponResult.fullCouponDeduction;
+ usedCoupon = couponResult.usedCoupon;
+ excludedProductIds = couponResult.excludedProductIds;
+
+ // 若满减与优惠券同享(couponShare=1),才计算优惠券;否则优惠券抵扣为0
+ if (
+ usedFullReductionThreshold &&
+ (!usedFullReductionActivity || !usedFullReductionActivity.couponShare)
+ ) {
+ couponDeductionAmount = 0;
+ productCouponDeduction = 0;
+ fullCouponDeduction = 0;
+ usedCoupon = undefined;
+ excludedProductIds = [];
+ }
+
+ // ------------------------------ 4. 积分抵扣(适配满减同享规则) ------------------------------
+ let pointDeductionAmount = 0;
+ let usedPoints = 0;
+
+ // 计算积分抵扣基数(商品折扣后 - 新客立减 - 满减 - 优惠券 + 餐位费 + 打包费 + 附加费)
+ let maxPointDeductionLimit = new BigNumber(goodsRealAmount)
+ .minus(newUserDiscount)
+ .minus(fullReductionAmount)
+ .minus(couponDeductionAmount)
+ .plus(seatFee)
+ .plus(packFee)
+ .plus(additionalFee)
+ .toNumber();
+ maxPointDeductionLimit =
+ maxPointDeductionLimit > 0 ? maxPointDeductionLimit : 0;
+
+ const pointResult = calcPointDeduction(
+ config.userPoints,
+ config.pointDeductionRule,
+ maxPointDeductionLimit
+ );
+
+ pointDeductionAmount = pointResult.deductionAmount;
+ usedPoints = pointResult.usedPoints;
+ // 若满减与积分不同享(pointsShare=1)积分抵扣为0
+ if (
+ usedFullReductionThreshold &&
+ (!usedFullReductionActivity || !usedFullReductionActivity.pointsShare)
+ ) {
+ pointDeductionAmount = 0;
+ usedPoints = 0;
+ }
+
+ //使用霸王餐
+ if (isFreeDine && freeDineConfig && freeDineConfig.enable) {
+ fullReductionAmount = 0;
+ //不与优惠券同享
+ if (!freeDineConfig.withCoupon) {
+ couponDeductionAmount = 0;
+ productCouponDeduction = 0;
+ fullCouponDeduction = 0;
+ usedCoupon = undefined;
+ excludedProductIds = [];
+ }
+ //不与积分同享
+ if (!freeDineConfig.withPoints) {
+ pointDeductionAmount = 0;
+ usedPoints = 0;
+ }
+ }
+
+ // 商家减免计算(原有逻辑不变)
+ const merchantReductionConfig = config.merchantReduction;
+ let merchantReductionActualAmount = 0;
+ const maxMerchantReductionLimit = new BigNumber(goodsRealAmount)
+ .minus(newUserDiscount)
+ .minus(fullReductionAmount)
+ .minus(couponDeductionAmount)
+ .minus(pointDeductionAmount)
+ .plus(seatFee)
+ .plus(packFee)
+ .isGreaterThan(0)
+ ? new BigNumber(goodsRealAmount)
+ .minus(newUserDiscount)
+ .minus(fullReductionAmount)
+ .minus(couponDeductionAmount)
+ .minus(pointDeductionAmount)
+ .plus(seatFee)
+ .plus(packFee)
+ .toNumber()
+ : 0;
+
+ switch (merchantReductionConfig.type) {
+ case MerchantReductionType.FIXED_AMOUNT:
+ merchantReductionActualAmount = Math.min(
+ merchantReductionConfig.fixedAmount || 0,
+ maxMerchantReductionLimit
+ );
+ break;
+ case MerchantReductionType.DISCOUNT_RATE:
+ const validRate =
+ Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0)) /
+ 100;
+ merchantReductionActualAmount =
+ maxMerchantReductionLimit * (1 - validRate);
+ break;
+ }
+ merchantReductionActualAmount = Math.max(
+ 0,
+ truncateToTwoDecimals(merchantReductionActualAmount)
+ );
+
+ // 会员折扣减免
+ const vipDiscountAmount = calcVipDiscountAmount(
+ new BigNumber(goodsRealAmount)
+ .minus(couponDeductionAmount)
+ .plus(packFee)
+ .plus(seatFee)
+ .minus(newUserDiscount)
+ .minus(fullReductionAmount)
+ .toNumber(),
+ shopUserInfo
+ );
+ console.log("vipDiscountAmount", vipDiscountAmount);
+ // ------------------------------ 6. 最终实付金额计算 ------------------------------
+ const finalPayAmountBn = new BigNumber(goodsRealAmount)
+ .minus(newUserDiscount)
+ .minus(vipDiscountAmount)
+ .minus(fullReductionAmount)
+ .minus(couponDeductionAmount)
+ .minus(pointDeductionAmount)
+ .minus(merchantReductionActualAmount)
+ .plus(seatFee)
+ .plus(packFee)
+ .plus(additionalFee);
+ let finalPayAmount = Math.max(
+ 0,
+ truncateToTwoDecimals(finalPayAmountBn.toNumber())
+ );
+ // ------------------------------ 使用霸王餐计算 ------------------------------
+ let orderOriginFinalPayAmount = finalPayAmount;
+ if (isFreeDine && freeDineConfig && freeDineConfig.enable) {
+ finalPayAmount = BigNumber(finalPayAmount)
+ .times(freeDineConfig.rechargeTimes)
+ .toNumber();
+ }
+
+ // ------------------------------ 7. 总优惠金额计算 ------------------------------
+ const totalDiscountAmount = truncateToTwoDecimals(
+ new BigNumber(goodsDiscountAmount)
+ .plus(newUserDiscount)
+ .plus(fullReductionAmount)
+ .plus(couponDeductionAmount)
+ .plus(pointDeductionAmount)
+ .plus(merchantReductionActualAmount)
+ .plus(vipDiscountAmount)
+ .toNumber()
+ );
+ //积分可抵扣最大金额 最终支付金额+积分抵扣-商家减免
+ const scoreMaxMoney = new BigNumber(finalPayAmount)
+ .plus(pointDeductionAmount)
+ .minus(merchantReductionActualAmount)
+ .toNumber();
+
+ // ------------------------------ 8. 返回完整结果 ------------------------------
+ return {
+ goodsTotal: goodsList.reduce(
+ (sum, g) => sum + Math.max(0, g.number - (g.returnNum || 0)),
+ 0
+ ),
+ goodsRealAmount,
+ goodsOriginalAmount,
+ goodsDiscountAmount,
+ couponDeductionAmount,
+ productCouponDeduction,
+ fullCouponDeduction,
+ pointDeductionAmount,
+ seatFee,
+ packFee,
+ totalDiscountAmount,
+ //最终支付原金额
+ orderOriginFinalPayAmount,
+ //积分最大可抵扣金额
+ scoreMaxMoney,
+ // 满减活动明细(后端字段)
+ fullReduction: {
+ usedFullReductionActivityFullAmount: usedFullReductionActivityFullAmount,
+ usedActivity: usedFullReductionActivity,
+ usedThreshold: usedFullReductionThreshold,
+ actualAmount: truncateToTwoDecimals(fullReductionAmount),
+ },
+ vipDiscountAmount: vipDiscountAmount, //会员折扣减免金额
+ merchantReduction: {
+ type: merchantReductionConfig.type,
+ originalConfig: merchantReductionConfig,
+ actualAmount: merchantReductionActualAmount,
+ },
+ additionalFee,
+ finalPayAmount,
+ couponUsed: usedCoupon,
+ pointUsed: usedPoints,
+ newUserDiscount,
+ dinnerType,
+ config,
+ };
+}
+
+export function isWeightGoods(goods: BaseCartItem): boolean {
+ return goods.product_type === GoodsType.WEIGHT;
+}
+
+// ============================ 7. 对外暴露工具库 ============================
+export const OrderPriceCalculator = {
+ // 基础工具
+ truncateToTwoDecimals,
+ isTemporaryGoods,
+ isGiftGoods,
+ formatDiscountRate,
+ filterThresholdGoods,
+ isWeightGoods,
+ // 商品价格计算
+ calcSingleGoodsRealPrice,
+ calcGoodsOriginalAmount,
+ calcGoodsRealAmount,
+ calcGoodsDiscountAmount,
+ //满减活动工具
+ filterOptimalFullReductionActivity,
+ // 优惠券计算
+ calcCouponDeduction,
+ // 其他费用计算
+ calcTotalPackFee,
+ calcSeatFee,
+ calcPointDeduction,
+ // 核心入口
+ calculateOrderCostSummary,
+ // 枚举导出
+ Enums: {
+ GoodsType,
+ CouponType,
+ ActivityType,
+ WEEKDAY_MAP,
+ },
+};
+
+export default OrderPriceCalculator;
diff --git a/utils/order-utils.js b/utils/order-utils.js
new file mode 100644
index 0000000..97477af
--- /dev/null
+++ b/utils/order-utils.js
@@ -0,0 +1,170 @@
+import BigNumber from "bignumber.js";
+
+//判断商品是否可以使用限时折扣
+export function canUseLimitTimeDiscount(
+ goods,
+ limitTimeDiscountRes,
+ shopInfo,
+ shopUserInfo,
+ idKey = "id"
+) {
+ shopInfo = shopInfo || {};
+ shopUserInfo = shopUserInfo || {};
+ if (!limitTimeDiscountRes || !limitTimeDiscountRes.id) {
+ return false;
+ }
+
+ const canUseFoods = (limitTimeDiscountRes.foods || "").split(",");
+
+ const goodsCanUse =
+ limitTimeDiscountRes.foodType == 1 ||
+ canUseFoods.includes(`${goods[idKey]}`);
+ if (!goodsCanUse) {
+ return false;
+ }
+ if (limitTimeDiscountRes.discountPriority == "limit-time") {
+ return true;
+ }
+ if (limitTimeDiscountRes.discountPriority == "vip-price") {
+ if (shopUserInfo.isVip != 1 || shopUserInfo.isMemberPrice != 1) {
+ return true;
+ }
+
+ if (
+ shopUserInfo.isVip == 1 &&
+ shopUserInfo.isMemberPrice == 1 &&
+ goods.memberPrice * 1 <= 0
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * 返回商品显示价格
+ * @params {*} args 参数对象
+ * @params {*} args.goods 商品对象
+ * @params {*} args.shopInfo 店铺信息
+ * @params {*} args.limitTimeDiscountRes 限时折扣信息
+ * @params {*} args.shopUserInfo 店铺用户信息
+ * @returns
+ */
+export function returnPrice(args) {
+ let {
+ goods,
+ shopInfo,
+ limitTimeDiscountRes,
+ shopUserInfo,
+ idKey = "product_id",
+ } = args;
+ limitTimeDiscountRes=limitTimeDiscountRes||{foods:'',foodType:2}
+ const canUseFoods = (limitTimeDiscountRes.foods || "").split(",");
+ const includesGoods =
+ limitTimeDiscountRes.foodType == 1 ||
+ canUseFoods.includes("" + goods[idKey]);
+ shopInfo = shopInfo || {};
+ shopUserInfo = shopUserInfo || {};
+ if (shopUserInfo.isMemberPrice == 1 && shopUserInfo.isVip == 1 && shopInfo.isMemberPrice==1) {
+ const memberPrice = goods.memberPrice || goods.salePrice;
+
+ //是会员而且启用会员价
+ if (limitTimeDiscountRes) {
+ //使用限时折扣
+ //限时折扣优先
+ if (limitTimeDiscountRes.discountPriority == "limit-time") {
+ if (includesGoods) {
+ return returnLimitPrice({
+ price: goods.salePrice,
+ limitTimeDiscountRes,
+ });
+ } else {
+ return memberPrice;
+ }
+ }
+ if (
+ limitTimeDiscountRes.discountPriority == "vip-price" &&
+ includesGoods
+ ) {
+ if (goods.memberPrice * 1 > 0) {
+ //会员优先
+ return memberPrice;
+ } else {
+ const price = returnLimitPrice({
+ price: goods.salePrice,
+ limitTimeDiscountRes,
+ goods: goods,
+ });
+
+ return price;
+ }
+ } else {
+ return memberPrice;
+ }
+ } else {
+ //是会员没有限时折扣
+ return memberPrice;
+ }
+ } else {
+ // console.log('不是会员或者没有启用会员价',goods,limitTimeDiscountRes);
+ //不是会员或者没有启用会员价
+ if (limitTimeDiscountRes && limitTimeDiscountRes.id && includesGoods) {
+ const price = returnLimitPrice({
+ price: goods.salePrice,
+ limitTimeDiscountRes,
+ goods: goods,
+ });
+
+ return price;
+ } else {
+ return goods.salePrice;
+ }
+ }
+}
+
+/**
+ * 返回限时折扣价格
+ * @params {*} args 参数对象
+ * @params {*} args.limitTimeDiscountRes 限时折扣信息
+ * @params {*} args.price 商品价格
+ * @param {*} args.goods 商品对象
+ * @returns
+ */
+export function returnLimitPrice(args) {
+ const { limitTimeDiscountRes, price, goods } = args;
+ const discountRate = new BigNumber(
+ limitTimeDiscountRes.discountRate
+ ).dividedBy(100);
+
+ const result = BigNumber(price)
+ .times(discountRate)
+ .decimalPlaces(2, BigNumber.ROUND_UP)
+ .toNumber();
+ return result;
+}
+
+/**
+ * 判断是否返回会员价
+ * @param {*} args 参数对象
+ * @param {*} args.shopInfo 店铺信息
+ * @param {*} args.shopUserInfo 店铺用户信息
+ * @returns
+ */
+export function canReturnMemberPrice(args) {
+ const { shopInfo, shopUserInfo } = args;
+ if (shopUserInfo.isMemberPrice == 1 && shopUserInfo.isVip == 1) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/**
+ * 返回会员价格
+ * @param {*} goods
+ * @returns
+ */
+export function returnMemberPrice(goods) {
+ return goods.memberPrice || goods.salePrice;
+}
diff --git a/utils/pay.js b/utils/pay.js
index 43b4c8c..9bee02a 100644
--- a/utils/pay.js
+++ b/utils/pay.js
@@ -1,57 +1,57 @@
export const pay = (res) => {
- return new Promise((resolve, reject) => {
- uni.showLoading({
- title: '支付中...',
- mask: true
- })
- uni.requestPayment({
- // #ifdef MP-WEIXIN
- provider: 'wxpay', //支付类型-固定值
- partnerid: res.appId, // 微信支付商户号
- timeStamp: res.timeStamp, // 时间戳(单位:秒)
- nonceStr: res.nonceStr, // 随机字符串
- package: res.package, // 固定值
- signType: res.signType, //固定值
- paySign: res.paySign, //签名
- // #endif
- // #ifdef MP-ALIPAY
- provider: 'alipay', //支付类型-固定值
- orderInfo: res.tradeNo, // 微信支付商户号
- // #endif
- success: (res) => {
- console.log('pay');
- console.log(res);
- uni.hideLoading()
- // #ifdef MP-WEIXIN
- uni.showToast({
- title: "支付成功",
- icon:'none'
- })
- console.log('支付成功')
- resolve(res)
- // #endif
- // #ifdef MP-ALIPAY
- if (res.resultCode == '9000') {
- uni.showToast({
- title: "支付成功",
- icon:'none'
- })
- resolve(res)
- } else {
- uni.showToast({
- title: "支付失败",
- icon:'none'
- })
- }
- // #endif
- },
- fail: (res) => {
- setTimeout(() => {
- uni.hideLoading()
- }, 1000)
- reject(false)
- },
-
- });
- })
-}
\ No newline at end of file
+ return new Promise((resolve, reject) => {
+ uni.showLoading({
+ title: "支付中...",
+ mask: true,
+ });
+ uni.requestPayment({
+ // #ifdef MP-WEIXIN
+ provider: "wxpay", //支付类型-固定值
+ partnerid: res.appId, // 微信支付商户号
+ timeStamp: res.timeStamp, // 时间戳(单位:秒)
+ nonceStr: res.nonceStr, // 随机字符串
+ package: res.package, // 固定值
+ signType: res.signType, //固定值
+ paySign: res.paySign, //签名
+ // #endif
+ // #ifdef MP-ALIPAY
+ provider: "alipay", //支付类型-固定值
+ orderInfo: res.tradeNo, // 微信支付商户号
+ // #endif
+ success: (res) => {
+ console.log("pay");
+ console.log(res);
+ uni.hideLoading();
+ // #ifdef MP-WEIXIN
+ uni.showToast({
+ title: "支付成功",
+ icon: "none",
+ });
+ console.log("支付成功");
+ resolve(true);
+ // #endif
+ // #ifdef MP-ALIPAY
+ if (res.resultCode == "9000") {
+ uni.showToast({
+ title: "支付成功",
+ icon: "none",
+ });
+ resolve(true);
+ } else {
+ uni.showToast({
+ title: "支付失败",
+ icon: "none",
+ });
+ reject(false);
+ }
+ // #endif
+ },
+ fail: (res) => {
+ setTimeout(() => {
+ uni.hideLoading();
+ }, 1000);
+ reject(false);
+ },
+ });
+ });
+};
diff --git a/utils/uniapp.js b/utils/uniapp.js
index 4803be2..cce0492 100644
--- a/utils/uniapp.js
+++ b/utils/uniapp.js
@@ -1,5 +1,5 @@
export const back=()=>{
- console.log('back');
+ console.log('调用返回方法back');
try {
const arr= getCurrentPages()
if(arr.length>=2){
diff --git a/utils/util.js b/utils/util.js
new file mode 100644
index 0000000..9ba2245
--- /dev/null
+++ b/utils/util.js
@@ -0,0 +1,143 @@
+/**
+ * 手机号脱敏:隐藏中间4位(11位手机号通用)
+ * @param {string} phone - 原始手机号(可带非数字字符,如138-1234-5678)
+ * @returns {string} 脱敏后手机号
+ */
+export function desensitizePhone(phone) {
+ // 1. 提取纯数字(过滤非数字字符)
+ const purePhone = (phone || "").replace(/[^\d]/g, "");
+
+ // 2. 边界判断:非11位手机号返回原字符串(或自定义提示)
+ if (purePhone.length !== 11) {
+ console.warn("手机号格式不正确,需11位纯数字");
+ return phone; // 或返回 ''、'手机号格式错误' 等
+ }
+
+ // 3. 脱敏:前3位 + **** + 后4位
+ return purePhone.replace(/(\d{3})(\d{4})(\d{4})/, "$1****$3");
+ }
+
+ /**
+ * 姓名合法性校验
+ * @param {string} name - 待校验的姓名
+ * @returns {Object} 校验结果:{ valid: boolean, msg: string }
+ */
+export function validateName(name) {
+ // 1. 空值校验
+ if (!name || name.trim() === '') {
+ return { valid: false, msg: '姓名不能为空' };
+ }
+ const pureName = name.trim();
+
+ // 2. 长度校验(2-6位,含少数民族中间点)
+ if (pureName.length < 2 || pureName.length > 6) {
+ return { valid: false, msg: '姓名长度应为2-6位' };
+ }
+
+ // 3. 正则校验:仅允许中文、少数民族中间点(·),且中间点不能在开头/结尾
+ // 中文范围:[\u4e00-\u9fa5],中间点:[\u00b7](Unicode 标准中间点,非小数点)
+ const nameReg = /^[\u4e00-\u9fa5]+([\u00b7][\u4e00-\u9fa5]+)*$/;
+ if (!nameReg.test(pureName)) {
+ return {
+ valid: false,
+ msg: '姓名仅支持中文和少数民族中间点(·),且不能包含数字、字母或特殊符号'
+ };
+ }
+
+ // 4. 额外限制:中间点不能连续(如“李··四”)
+ if (/[\u00b7]{2,}/.test(pureName)) {
+ return { valid: false, msg: '姓名中的中间点(·)不能连续' };
+ }
+
+ // 校验通过
+ return { valid: true, msg: '姓名格式合法' };
+}
+
+/**
+ * 身份证号码合法性校验(支持18位/15位)
+ * @param {string} idCard - 待校验的身份证号
+ * @returns {Object} 校验结果:{ valid: boolean, msg: string, info?: Object }
+ * info 可选返回:{ birthDate: string, gender: string }(出生日期、性别)
+ */
+export function validateIdCard(idCard) {
+ // 1. 空值校验
+ if (!idCard || idCard.trim() === '') {
+ return { valid: false, msg: '身份证号码不能为空' };
+ }
+ const pureIdCard = idCard.trim().toUpperCase(); // 统一转为大写(处理X)
+
+ // 2. 格式校验(18位或15位)
+ const id18Reg = /^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))((0[1-9])|([12]\d)|(3[01]))\d{3}([0-9]|X)$/;
+ const id15Reg = /^[1-9]\d{5}\d{2}((0[1-9])|(1[0-2]))((0[1-9])|([12]\d)|(3[01]))\d{3}$/;
+
+ if (!id18Reg.test(pureIdCard) && !id15Reg.test(pureIdCard)) {
+ return {
+ valid: false,
+ msg: '身份证号码格式错误(需18位,最后一位可含X;或15位纯数字)'
+ };
+ }
+
+ // 3. 提取出生日期并校验合法性
+ let birthDateStr, birthDate;
+ if (pureIdCard.length === 18) {
+ // 18位:第7-14位为出生日期(YYYYMMDD)
+ birthDateStr = pureIdCard.slice(6, 14);
+ birthDate = new Date(`${birthDateStr.slice(0,4)}-${birthDateStr.slice(4,6)}-${birthDateStr.slice(6,8)}`);
+ } else {
+ // 15位:第7-12位为出生日期(YYMMDD),补全为YYYYMMDD(19xx或20xx,默认19xx)
+ const year = `19${pureIdCard.slice(6, 8)}`;
+ const month = pureIdCard.slice(8, 10);
+ const day = pureIdCard.slice(10, 12);
+ birthDateStr = `${year}${month}${day}`;
+ birthDate = new Date(`${year}-${month}-${day}`);
+ }
+
+ // 校验出生日期有效性(如20230230 → 日期对象会是Invalid Date)
+ if (
+ isNaN(birthDate.getTime()) ||
+ birthDateStr.slice(0,4) !== birthDate.getFullYear().toString() ||
+ birthDateStr.slice(4,6) !== (birthDate.getMonth() + 1).toString().padStart(2, '0') ||
+ birthDateStr.slice(6,8) !== birthDate.getDate().toString().padStart(2, '0')
+ ) {
+ return { valid: false, msg: '身份证中的出生日期无效' };
+ }
+
+ // 4. 18位身份证额外校验:校验码合法性(加权算法)
+ if (pureIdCard.length === 18) {
+ // 加权因子
+ const weightFactors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
+ // 校验码对应值(0-10 → 10对应X)
+ const checkCodeMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
+ // 计算前17位与加权因子的乘积和
+ let sum = 0;
+ for (let i = 0; i < 17; i++) {
+ sum += parseInt(pureIdCard[i]) * weightFactors[i];
+ }
+ // 计算预期校验码
+ const expectedCheckCode = checkCodeMap[sum % 11];
+ // 对比实际校验码(最后一位)
+ if (pureIdCard[17] !== expectedCheckCode) {
+ return { valid: false, msg: '身份证校验码错误,可能是无效身份证' };
+ }
+ }
+
+ // 5. 可选:提取性别(18位第17位,15位第15位;奇数=男,偶数=女)
+ let gender = '';
+ if (pureIdCard.length === 18) {
+ const genderCode = parseInt(pureIdCard[16]);
+ gender = genderCode % 2 === 1 ? '男' : '女';
+ } else {
+ const genderCode = parseInt(pureIdCard[14]);
+ gender = genderCode % 2 === 1 ? '男' : '女';
+ }
+
+ // 校验通过,返回额外信息(出生日期、性别)
+ return {
+ valid: true,
+ msg: '身份证号码合法',
+ info: {
+ birthDate: `${birthDate.getFullYear()}-${(birthDate.getMonth() + 1).toString().padStart(2, '0')}-${birthDate.getDate().toString().padStart(2, '0')}`,
+ gender: gender
+ }
+ };
+}
\ No newline at end of file