92 Commits

Author SHA1 Message Date
gxwebsoft 8d8080c7bf feat(shopZone): 新增专区折扣系数功能及购物车链路支持
- HomeSection 模型增加 discountRate 字段,默认 1.00 表示不打折
- zoneEdit 组件新增折扣系数输入框,支持设置 0.01~1 之间的折扣率
- 后端 guilixu-java 增加专区折扣逻辑,成交价=市场价×折扣系数,保证后端价格计算安全
- 小程序 guilixu-taro 频道详情和商品详情支持专区折扣展示和传参
- 购物车新增 sectionId 字段,支持专区折扣价计算,完善购物车加购与价格计算逻辑
- 提供对应迁移 SQL 脚本更新数据库结构(专区折扣率与购物车 section_id)
- 修复历史订单商品支付状态,补偿专区销量统计数据
- MEMORY 文档补充详细开发与设计说明,明确双后端接口调度和业务逻辑划分
2026-08-18 10:53:15 +08:00
gxwebsoft 5943f1536d chore(env): 更换后台管理系统 API 接口地址为 guilixu-api.websoft.top
- 修改开发环境与生产环境的 VITE_API_URL 为 https://guilixu-api.websoft.top/api
- 更新内存文件中关于商城接口实际调用后端域名为 guilixu-api.websoft.top 的描述
- 修正后端待部署地址及下单接口调用链接指向 guilixu-api.websoft.top
- 统一接口调用基地址,明确 guilixu-java 为生效的主后端服务
- 记录用户纠正信息,强调运行时 ApiUrl 配置优先级与实际生效后端一致
2026-08-18 01:00:25 +08:00
gxwebsoft bec3a6f1b8 feat(special-zone): 走压缩URL处理专区商品图片
- 将专区商品抽屉已关联商品列表的图片src改为压缩URL,减少缩略图体积
- 调整banner图片压缩宽度为300,保证清晰度与加载速度平衡
- 统一调用getCompressedImageUrl方法,无需额外引入
- 优化图片加载性能,提升用户体验
2026-08-17 16:47:37 +08:00
gxwebsoft 65926a35b6 fix(special-zone): 修复专区商品抽屉分页点击无效
- 将分页配置从内联对象改为计算属性,避免分页参数被 Vue 不自动解包的问题
- goodsPagination 计算属性返回正确的分页参数并绑定 onChange 事件
- onChange 中调用 loadGoods 时修正 currentSectionId 使用 .value 获取
- 调整商品操作列宽度以适应界面显示
- 验证 vite build 通过,分页功能正常响应点击
2026-08-17 14:50:31 +08:00
gxwebsoft ced8c7cff7 feat(shop-zone): 新增专区销量统计功能
- 后端新增专区销量统计相关 VO 并扩展 ShopHomeSection 实体
- 新增批量查询专区销量汇总与商品排行的 Mapper 方法及接口
- Controller 增加获取专区销量统计的接口支持时间范围查询
- 前端接口定义新增专区销量统计类型及请求方法
- 专区管理页面列表新增销量件数、销售额列及销量详情按钮
- 实现专区销量统计弹窗支持时间筛选、汇总展示和排行显示
- 完成前端相关界面和交互设计,保证功能完整可用
- 统计口径基于已支付且未取消/退款订单商品实际成交数据
2026-08-17 12:40:08 +08:00
gxwebsoft fe97f49afe fix(shop): 修复商城商品导出及专区商品分页问题
- 修复专区商品抽屉分页中20/100条/页按键无效,增加动态分页参数支持
- 修复商城商品导出功能无反应问题,调整loading初始值保证导出流程执行
- 支持商品导出携带当前筛选条件,保证导出数据符合筛选结果
- 商品导出增加封面图路径列,导出URL文本方式避免跨域限制
- 优化UserSelectModal手机号码展示,改为优先展示真实号码字段
- 相关组件调整分页及导出事件参数,提升交互准确性与稳定性
2026-08-17 12:09:38 +08:00
gxwebsoft 6556944ffc fix(shop): 修复商城商品导出及专区商品分页问题
- 修复专区商品抽屉分页中20/100条/页按键无效,增加动态分页参数支持
- 修复商城商品导出功能无反应问题,调整loading初始值保证导出流程执行
- 支持商品导出携带当前筛选条件,保证导出数据符合筛选结果
- 商品导出增加封面图路径列,导出URL文本方式避免跨域限制
- 优化UserSelectModal手机号码展示,改为优先展示真实号码字段
- 相关组件调整分页及导出事件参数,提升交互准确性与稳定性
2026-08-17 11:34:39 +08:00
gxwebsoft 3c4e30c227 fix(user-select-modal): 专区白名单手机号改为不脱敏显示
- UserSelectModal.vue 中候选行手机号由 u.mobile 改为 u.phone || u.mobile || '-'
- 表格列手机号由 dataIndex: 'mobile' 改为 dataIndex: 'phone'
- 表格列 customRender 实现回退到 record.mobile 显示真实号码
- 无需后端改动,只需重新构建 admin 前端进行部署
2026-08-17 03:26:07 +08:00
gxwebsoft 47fb8d1bd0 feat(zone): 将专区白名单弹窗改为右侧抽屉
- 用户要求专区白名单入口交互与商品一致,改为右侧抽屉弹出
- UserSelectModal 组件外层由 a-modal 改为 a-drawer,保持内部逻辑不变
- 组件 props 和 emits 完全兼容,调用方式无需更改
- 前端重新构建即可,无需后端改动
2026-08-17 03:23:45 +08:00
gxwebsoft 4ba0427417 feat(special-zone): 优化专区白名单弹窗及添加/移除功能
- 修复白名单弹窗复选框不显示的问题,替换 ele-pro-table 为 antd a-table 实现
- 实现弹窗打开即加载已选用户并勾选,完善分页及搜索功能
- 解决弹窗搜索事件传参错误导致后端参数异常的问题
- 修复保存白名单时报唯一键冲突异常,改用物理删除规避逻辑删除死锁
- 重构白名单弹窗为商品式交互,支持即时单条添加和移除,无需覆盖式保存
- 后端新增接口支持白名单用户追加和删除,前端调用对应新增接口
- 统一用户模型字段,优化查询及展示,提升用户体验
- 清理前端无用代码,简化白名单弹窗调用逻辑,减少页面复杂度
2026-08-17 03:10:30 +08:00
gxwebsoft 6e57fba192 fix(special-zone): 修复白名单弹窗checkbox未显示问题并添加专区小程序码功能
- 修复 UserSelectModal 弹窗中复选框不显示的根因,正确传入 selection-type="checkbox"
- 新增后台接口获取专区小程序码图片流,支持正式/体验/开发版环境
- 管理后台专区列表新增二维码按钮,弹窗展示对应专区小程序码
- 小程序端支持扫码参数 scene 解析,实现扫码直接进入专区页面
- 优化二维码弹窗组件,支持切换小程序版本并动态加载二维码
- 重新构建前端及后端服务,确保新功能生效和兼容原有业务逻辑
2026-08-17 02:42:13 +08:00
gxwebsoft d458115da9 fix(special-zone): 修复白名单弹窗checkbox未显示问题并添加专区小程序码功能
- 修复 UserSelectModal 弹窗中复选框不显示的根因,正确传入 selection-type="checkbox"
- 新增后台接口获取专区小程序码图片流,支持正式/体验/开发版环境
- 管理后台专区列表新增二维码按钮,弹窗展示对应专区小程序码
- 小程序端支持扫码参数 scene 解析,实现扫码直接进入专区页面
- 优化二维码弹窗组件,支持切换小程序版本并动态加载二维码
- 重新构建前端及后端服务,确保新功能生效和兼容原有业务逻辑
2026-08-17 02:39:52 +08:00
gxwebsoft f986efce8c fix(api): 修复专区白名单和商品移除假成功问题
- 调整 .env.development 中 VITE_API_URL 配置为正式接口地址
- 修复 ShopGoods.sectionIds 字段更新时 null 被忽略导致移除专区商品假成功问题
- 通过 UpdateWrapper 显式设置 section_ids 字段避免 MyBatis-Plus 全局更新策略影响
- 补充后端下单接口白名单强校验,防止绕过前端受限专区限制
- 澄清买家用户体系为 sys_user,确认白名单弹窗使用 pageUsers 数据源正确
- 删除白名单弹窗 isStaff 限制,白名单覆盖所有 sys_user 用户
- 记录专区白名单功能设计与实现细节,明确前后端联动机制
- 提示“专区管理”菜单入口需后端菜单表动态下发,确保后台导航可见
2026-08-17 02:00:05 +08:00
gxwebsoft 4a7b217c2b feat(special-zone): 实现专区商品管理功能
- 新增添加商品到专区接口 addSectionGoods
- 新增从专区移除商品接口 removeSectionGoods
- 商品抽屉宽度调整并改造为支持搜索添加和移除商品
- 增加商品搜索输入框及搜索按钮,支持关键词搜索商品
- 搜索结果列表添加单条商品添加按钮和批量添加确认按钮
- 已关联商品列表新增移除商品操作按钮及确认弹窗
- 调整商品列表图片大小及表格滚动高度,提高界面整洁度
- 专区商品数据加载添加搜索结果过滤逻辑,避免重复添加
- 用户白名单弹窗移除 isStaff 限制,允许所有用户搜索
- 修复白名单用户选择框无法显示缺失勾选列宽度问题
- 采用 computed 管理多选配置及已选用户状态,提升响应性
2026-08-17 01:14:16 +08:00
gxwebsoft b5c67988a6 feat(shop): 新增首页专区管理功能
- 在商品模型中添加所属专区字段 sectionIds,支持逗号分隔字符串
- 商品编辑界面增加所属专区多选下拉框,联动专区数据
- 新增首页专区相关接口,包括分页查询、增删改查及权限校验等
- 实现首页专区列表页面,支持专区的增删改查及状态切换
- 新增专区白名单用户管理弹窗,支持多选用户设置白名单
- 实现专区商品列表抽屉,支持分页查看专区内商品
- 首页专区编辑弹窗支持标题、副标题、banner等多字段编辑及上传功能
- 完善专区管理界面交互和表格展示,支持搜索和状态筛选功能
2026-08-15 03:30:48 +08:00
gxwebsoft 053b776d9a feat(shopGoods): 支持商品分类多选及列表树形高度优化
- 商品模型增加 categoryIds 字段支持多分类,搜索参数同步支持多分类查询
- 商品列表新增分类列,展示商品所属分类名称
- 商品编辑及搜索组件商品分类选择改为多选,保持兼容单选主分类逻辑
- 商品分类树形列表SVG连线高度调小,提升界面紧凑度
- 优化分类图片尺寸,缩小预览图大小,提升视觉效果
- 更新用户推荐接口方法参数类型声明,增加类型安全保障
2026-08-14 19:38:02 +08:00
gxwebsoft 780606f05a feat(shopGoodsCategory): 新增商品分类编辑弹窗“链接地址”字段
- 在分类标识下方添加“链接地址”输入项,支持清除和占位提示
- 新增字段绑定到表单数据的path属性,确保表单数据同步更新
- 重置表单时初始化path字段为undefined,保证新增分类时清空该字段
- 修改仅限前端界面,后端数据库已有path字段,无需额外迁移
2026-08-14 18:14:22 +08:00
gxwebsoft a04a8baec7 feat(shopExpressTemplateDetail): 支持「不发货黑名单」配送模式
- 新增配送模式三态选择(0全国/1指定配送/2指定不发货)
- 编辑表单中增加“配送模式”单选,mode=2时复用多选地区组件选择不发货地区
- 保存时根据模式调整 regionMode 和 regionIds 字段,确保模式与地区同步
- 列表页配送地区列新增不发货黑名单显示,聚合城市码为省份名并支持省名去重
- 加载地区时建立城市ID到省份ID映射,实现城市码聚合显示省名功能
- 适配旧数据兼容性,支持单值旧数据和全国模式显示
- 表单校验强化,指定配送/不发货模式下必须选择有效地区
- 完成端到端链路功能闭环,前端支持黑名单配置,后端按租户生效配送限制
2026-08-12 14:36:43 +08:00
gxwebsoft 6a5b7ff711 feat(shopExpressTemplateDetail): 支持「不发货黑名单」配送模式
- 新增配送模式三态选择(0全国/1指定配送/2指定不发货)
- 编辑表单中增加“配送模式”单选,mode=2时复用多选地区组件选择不发货地区
- 保存时根据模式调整 regionMode 和 regionIds 字段,确保模式与地区同步
- 列表页配送地区列新增不发货黑名单显示,聚合城市码为省份名并支持省名去重
- 加载地区时建立城市ID到省份ID映射,实现城市码聚合显示省名功能
- 适配旧数据兼容性,支持单值旧数据和全国模式显示
- 表单校验强化,指定配送/不发货模式下必须选择有效地区
- 完成端到端链路功能闭环,前端支持黑名单配置,后端按租户生效配送限制
2026-08-12 10:27:47 +08:00
gxwebsoft 9b5875e6fb feat(shopExpressTemplateDetail): 支持「不发货黑名单」配送模式
- 新增配送模式三态选择(0全国/1指定配送/2指定不发货)
- 编辑表单中增加“配送模式”单选,mode=2时复用多选地区组件选择不发货地区
- 保存时根据模式调整 regionMode 和 regionIds 字段,确保模式与地区同步
- 列表页配送地区列新增不发货黑名单显示,聚合城市码为省份名并支持省名去重
- 加载地区时建立城市ID到省份ID映射,实现城市码聚合显示省名功能
- 适配旧数据兼容性,支持单值旧数据和全国模式显示
- 表单校验强化,指定配送/不发货模式下必须选择有效地区
- 完成端到端链路功能闭环,前端支持黑名单配置,后端按租户生效配送限制
2026-08-12 10:01:03 +08:00
gxwebsoft 07add26fe6 refactor(cmsArticle): 统一文章编辑器为单一 WangEditor 富文本模式
- 删除原有 TinyMCE 富文本和 MdEditor Markdown 双模式切换及相关逻辑
- 替换文章编辑弹窗内容详情为 RichTextEditor 组件
- 新增图片库和视频库选择按钮,支持插入图片和视频 HTML 标签
- 实现一键排版和首行缩进按钮,直接操作内容 HTML 字符串
- 回显时 Markdown 内容转换为 HTML,保存时固定 editor 类型为 1
- 移除无用的配置和样式,简化代码逻辑,提升编辑体验
- 留下文章编辑器类型设置字段待后续清理,展示端保持一致 HTML 渲染方式
2026-08-11 21:58:09 +08:00
gxwebsoft cf7d065379 feat(shop): 商品管理编辑弹窗替换为WangEditor富文本编辑器
- 用户反馈编辑弹窗的Markdown编辑器体验差,采用website-admin的富文本编辑器替代
- 新增依赖@wangeditor/editor、@wangeditor/editor-for-vue、markdown-it及类型声明
- 新建RichTextEditor组件,去除Nuxt专用包装,适配现有文件上传接口
- shopGoodsEdit.vue移除MdEditor及相关冗余代码,改用RichTextEditor显示内容
- 实现编辑时Markdown内容转换为HTML,保持数据库存储格式新旧兼容
- 保存逻辑保持不变,提交内容为HTML格式
- 确认小程序端可兼容新旧内容格式,潜在HTML图片链接误解析风险
- build验证通过,文件产出正常,图片上传小程序域名白名单需上线后确认
2026-08-11 18:52:40 +08:00
gxwebsoft b6ae3d71ef feat(shopGoods): 支持多规格价格区间及修复快递配送设置保存问题
- 商品列表价格列新增多规格价格区间显示,单规格依旧显示单值
- 后端新增非持久化最大价字段 priceMax/salePriceMax/dealerPriceMax,批量读取SKU价格区间
- 前端 API 和列表价格列渲染及导出支持价格区间显示
- 添加快递配送相关字段 canExpress 和 expressTemplateId 至后端实体并新增对应数据库字段
- 调整前端运费模板表单项位置及必填校验逻辑,修复保存不生效问题
- 修改 SKU 图片删除方法,避免误删主商品图片数组
2026-08-11 16:22:38 +08:00
gxwebsoft 8ab3326f26 feat(shopGoods): 支持多规格价格区间及修复快递配送设置保存问题
- 商品列表价格列新增多规格价格区间显示,单规格依旧显示单值
- 后端新增非持久化最大价字段 priceMax/salePriceMax/dealerPriceMax,批量读取SKU价格区间
- 前端 API 和列表价格列渲染及导出支持价格区间显示
- 添加快递配送相关字段 canExpress 和 expressTemplateId 至后端实体并新增对应数据库字段
- 调整前端运费模板表单项位置及必填校验逻辑,修复保存不生效问题
- 修改 SKU 图片删除方法,避免误删主商品图片数组
2026-08-11 15:46:52 +08:00
gxwebsoft c85b1351e3 fix(shop): 修复多规格SKU图片删除问题
- 修正删除SKU图片时错误操作,防止误删主商品图片数组
- 删除时清空对应SKU行的图片数组与主图字段
- 优化运费模板标签,明确必填状态提示
- 添加会员价字段buyingPrice,更新相关TS接口及数据库结构说明
2026-08-11 11:44:32 +08:00
gxwebsoft 9de1df84ca fix(shop): 修复多规格SKU会员价保存失败问题
- 后端 ShopGoodsSku 实体类添加 buyingPrice 字段,解决数据丢失
- 前端接口定义增加 buyingPrice,保证数据完整同步
- mp-java 后端模块同步添加 buyingPrice 字段实现数据一致
- 需手动执行 SQL 新增数据库表 shop_goods_sku 的 buying_price 列
2026-08-11 10:55:51 +08:00
gxwebsoft 706db16337 fix(shopGoods): 修复SKU表格库存列和运费模板校验缺失
- 补充多规格SKU表格中缺失的划线价和库存列定义
- 添加快递配送开启时运费模板前端必填校验
- 在保存逻辑中增加运费模板缺失时的错误提示
- 运费模板表单项动态显示必填标记及占位提示
- 保持与后端校验逻辑一致,提升用户体验
2026-08-11 10:52:29 +08:00
gxwebsoft 40bcc77c50 fix(shopExpressTemplateDetail): 修正配送区域多选字段对接后端机制
- model中省市多选字段由provinceIds/cityIds改为regionMode/regionIds
- RegionMultiSelect组件去除update:provinceIds事件,改仅emit update:value
- 编辑弹窗中城市选择变化同步更新form.regionIds和form.regionMode字段
- 回显数据时从regionIds拆分赋值,不再使用provinceIds/cityIds
- 列表页customRender展示使用record.regionIds反查城市名
- 解决前端提交字段与后端实体不匹配导致数据未保存问题
- 确保本次改动完全兼容后端现有regionMode/regionIds机制,无需后端改动
2026-08-11 10:38:21 +08:00
gxwebsoft a228dcb96b fix(shopExpressTemplateDetail): 修正配送区域多选字段对接后端机制
- model中省市多选字段由provinceIds/cityIds改为regionMode/regionIds
- RegionMultiSelect组件去除update:provinceIds事件,改仅emit update:value
- 编辑弹窗中城市选择变化同步更新form.regionIds和form.regionMode字段
- 回显数据时从regionIds拆分赋值,不再使用provinceIds/cityIds
- 列表页customRender展示使用record.regionIds反查城市名
- 解决前端提交字段与后端实体不匹配导致数据未保存问题
- 确保本次改动完全兼容后端现有regionMode/regionIds机制,无需后端改动
2026-08-11 10:32:57 +08:00
gxwebsoft ae2ea4d841 feat(shopExpressTemplateDetail): 支持省市两级多选配送区域
- 新增 model 字段 provinceIds 和 cityIds,兼容旧单值 provinceId 和 cityId
- 引入 RegionMultiSelect 组件替代旧的单选级联选择器
- 编辑弹窗支持多选城市和省份,提交时转换为逗号分隔字符串
- 列表页配送区域列支持多选展示,超长文本截断并显示 tooltip
- 新增 RegionMultiSelect 组件,实现省市两级树形多选穿梭弹窗
- 通过 computed 和 watch 同步组件选中状态和表单数据
- 更新相关类型定义和导入,确保编辑及展示逻辑正确
- 新增样式优化,提示默认配送全国规则说明
2026-08-11 09:01:30 +08:00
gxwebsoft aae0c03dd8 feat(shopGoods): 优化商品列表加载及封面图性能
- image.ts 中 getCompressedImageUrl 增加缓存,避免列表重渲染时重复计算压缩 URL
- shopGoods 页面用原生 img 标签替换 antdv a-image,支持浏览器懒加载减少首屏网络压力
- shopGoods 页面数据加载流程优化,取消表格自动首屏加载,改为手动触发,合并请求避免重复调用
- 首屏分类数据延迟加载,不阻塞首屏渲染,提升页面响应速度
- search.vue 中用 onMounted 替代 watch immediate,防止首次入场重复触发请求
- 新增商品封面图样式,保证图片大小固定且样式统一
2026-08-06 18:18:05 +08:00
gxwebsoft 3bc856fc95 feat(shopGoods): 优化商品列表加载及封面图性能
- image.ts 中 getCompressedImageUrl 增加缓存,避免列表重渲染时重复计算压缩 URL
- shopGoods 页面用原生 img 标签替换 antdv a-image,支持浏览器懒加载减少首屏网络压力
- shopGoods 页面数据加载流程优化,取消表格自动首屏加载,改为手动触发,合并请求避免重复调用
- 首屏分类数据延迟加载,不阻塞首屏渲染,提升页面响应速度
- search.vue 中用 onMounted 替代 watch immediate,防止首次入场重复触发请求
- 新增商品封面图样式,保证图片大小固定且样式统一
2026-08-06 17:19:15 +08:00
gxwebsoft c553a71ea2 feat(dashboard): 新增后台今日数据概况及仪表盘页面
- 新增 shopOrderTotalAll 和 shopOrderOverview 接口,支持含未支付口径数据统计
- 新增 ShopOrderOverview 类型定义,包含今日销售额、订单数、新增用户数和优惠券使用数
- 新增后台专用仪表盘页面,展示用户数、订单数、总营业额和系统运行天数等核心数据
- 仪表盘页面实现新订单提醒功能,支持声音和语音播报
- 提供待付款、待发货订单数和退款申请动态展示,带点击跳转功能
- 新增今日数据概况区域,显示销售额、订单数、新增用户和优惠券使用情况
- 集成订阅和续费功能,支持支付二维码扫码支付及状态轮询
- 基本信息展示系统版本、授权状态以及订阅到期提醒和续费入口
- 快速入口模块方便访问订单管理、商品管理、优惠券管理等功能页
- 实现数据自动刷新机制,包括统计数据和含未支付订单的特殊统计
- 优化了界面样式和交互体验,提升了后台管理的可用性和操作便捷性
2026-08-01 22:11:27 +08:00
gxwebsoft a284a83b86 feat(dashboard): 新增后台今日数据概况及仪表盘页面
- 新增 shopOrderTotalAll 和 shopOrderOverview 接口,支持含未支付口径数据统计
- 新增 ShopOrderOverview 类型定义,包含今日销售额、订单数、新增用户数和优惠券使用数
- 新增后台专用仪表盘页面,展示用户数、订单数、总营业额和系统运行天数等核心数据
- 仪表盘页面实现新订单提醒功能,支持声音和语音播报
- 提供待付款、待发货订单数和退款申请动态展示,带点击跳转功能
- 新增今日数据概况区域,显示销售额、订单数、新增用户和优惠券使用情况
- 集成订阅和续费功能,支持支付二维码扫码支付及状态轮询
- 基本信息展示系统版本、授权状态以及订阅到期提醒和续费入口
- 快速入口模块方便访问订单管理、商品管理、优惠券管理等功能页
- 实现数据自动刷新机制,包括统计数据和含未支付订单的特殊统计
- 优化了界面样式和交互体验,提升了后台管理的可用性和操作便捷性
2026-08-01 22:10:28 +08:00
gxwebsoft bf60ba1d04 refactor(shop): 将商品价格相关文本统一修改为“商城价”
- 修改shopGoods、shopGoodsBrowse和shopGoodsSku模型中price字段注释
- 更新shopGoodsBrowse和shopGoodsSku页面表格中价格列标题
- 调整shopGoodsEdit组件中表单标签、输入框占位符和校验提示信息
- 同步修改shopGoodsSkuEdit组件中价格表单项的标签和占位符文本
- 保证所有相关文本表述一致,提升用户界面和代码的统一性
2026-07-30 12:45:27 +08:00
gxwebsoft eb26594f41 refactor(user): 优化用户列表展示和排序功能
- 表格大小设置为 small,提升视觉紧凑度
- 用户名称显示优先 realName,其次 nickname,缺失时显示 "-"
- 邮箱列添加排序功能,支持用户交互排序
- 移除创建时间列的文字省略,完整显示时间信息
2026-07-29 00:52:48 +08:00
gxwebsoft e87e60fd6a feat(shopOrder): 添加订单商品明细展开功能并优化订单状态显示
- 在订单列表中增加商品明细的展开/收起功能,默认显示2条商品信息
- 支持点击“展开全部 N 件”切换显示全部或折叠商品明细
- 优化订单状态栏支付状态、发货状态、开票状态及操作按钮中的分隔线统一风格
- 将shop/dashboard运行天数改为基于租户创建时间,而非订阅生效时间,保证准确性
- 调整订单列表表格列宽及显示顺序,改善界面布局和信息展示
- 注释和代码格式微调,删除无用的组件显示及注释代码,提升代码整洁度
- 修复了部分变量定义和模板标签排版,完善代码可维护性
2026-07-29 00:44:54 +08:00
gxwebsoft 4ab90c9d89 style(shop): 优化商品和订单列表界面细节
- 添加商品列表表格横向滚动支持,避免列溢出
- 设置商品图片高度,保持缩略图统一尺寸
- 修改商品状态标签文案为“出售中”
- 调整订单商品明细列,支持展开与收起全部商品
- 在订单列表操作列中新增竖线分隔符,提升按钮分隔清晰度
- 统一订单操作按钮文本,去除图标依赖
- 注释掉商品列表中排序号列
- 微调表格中部分列宽及对齐方式提高视觉体验
2026-07-28 19:44:12 +08:00
gxwebsoft a92248bff7 fix(api): 修复根据code查询租户时类型定义错误
- 修改getByCode函数中请求返回类型为ApiResult<Tenant>
- 修正对返回数据的访问以匹配新的类型结构
- 确保类型安全和避免潜在运行时错误
2026-07-27 14:30:56 +08:00
gxwebsoft dd094f8769 feat(cmsAd): 优化广告图片展示和引入图片压缩工具
- 使用压缩后的图片 URL 显示广告图片,提升加载性能
- 添加图片预览功能,支持点击查看原图
- 调整图片展示宽度参数,保证视觉效果
- 引入图片压缩工具方法 getCompressedImageUrl 以统一处理图片请求
2026-07-27 14:25:11 +08:00
gxwebsoft 4a8b0a957c refactor(api): 统一CMS接口URL配置
- 在环境变量中新增CMS接口URL配置项 VITE_CMS_API_URL
- 将所有CMS相关API接口调用的基地址由MODULES_API_URL或SERVER_API_URL替换为CMS_API_BASE_URL
- 更新cmsAd、cmsAdRecord、cmsArticle及相关cms模块的接口地址引用
- 统一管理CMS API基础路径,提高代码维护性和可配置性
2026-07-27 12:42:51 +08:00
gxwebsoft 45fc45680f feat(passport): 增加多租户手机号登录支持并修复邮箱验证码校验
- 修复邮箱登录验证码未校验问题,恢复输入错误提示及刷新验证码逻辑
- 新增多租户选项登录流程,支持单租户直接登录及多租户选择弹窗
- 扩展登录接口及模型,新增 TenantOption 类型和 LoginOutcome 联合结果类型
- 新增 loginBySelectTenant 二次登录接口及对应组件逻辑
- 统一保存登录态,包括 token、租户ID和用户ID到本地存储
- 调整前端短信登录逻辑,根据返回状态决定流程,支持多租户弹窗选择
- 确保新增代码无类型错误并通过构建验证
2026-07-24 22:33:39 +08:00
gxwebsoft 55e90c2434 feat(login): 优化手机号验证码发送逻辑,前3次免图形验证码
- 新增本地存储计数,记录每个手机号发送次数
- 前3次发送验证码无需弹出图形验证码弹窗,提升用户体验
- 第4次及以后需通过图形验证码验证后才发送短信
- 发送失败时自动刷新图形验证码并显示错误信息
- 补充异常处理,确保发送按钮状态正确重置
- 代码无类型错误,构建验证通过
2026-07-24 22:25:30 +08:00
gxwebsoft f9bf3c6df3 feat(passport): 优化忘记密码页发送验证码体验
- 引入图形验证码弹窗,风格与注册页面保持一致
- 使用 localStorage 记录每个账号发送验证码次数,前三次免图形验证码
- 第四次及以后发送验证码需输入正确图形验证码才能发送
- 失败时自动刷新验证码,关闭弹窗时重置加载状态
- 新增相关国际化文案支持中英文
- 调整登录页邮箱登录相关占位符文案提升用户体验
2026-07-24 22:14:29 +08:00
gxwebsoft d6b56d99ce feat(passport): 新增忘记密码功能及相关接口
- 新增忘记密码页面,支持邮箱找回和手机找回两种方式,包含验证码倒计时
- 添加密码强度和一致性校验,确保新密码安全性
- 开启对应路由 /forget,恢复已注释的忘记密码入口
- 新增发送短信验证码和邮箱验证码接口,适配未登录场景
- 新增重置密码接口,支持手机短信码及邮箱验证码方式
- 请求拦截器支持 X-Tenant-Skip 头,跳过租户信息注入,避免跨租户串扰
- 国际化新增忘记密码相关英文和中文文案
- 页面样式与登录页视觉风格一致,跨屏幕响应式布局优化
2026-07-24 22:07:53 +08:00
gxwebsoft 0bfd5e667b docs(shop): 补充商品分类多级分类调研内容
- 说明分类管理页支持多级层级及其实现方式
- 指出商品端选分类组件限制在2~3级的问题
- 提醒后端是否限制层级需查后端仓库确认

feat(shopGoods): 新增批量上架含图片商品SQL脚本

- 提供批量上架分类ID为10332且含有图片商品的SQL预览查询
- 包含更新商品状态为上架的SQL语句及执行说明
- 约定商品状态、图片判定及删除标记等业务规则
2026-07-24 11:19:50 +08:00
gxwebsoft a831dce61a feat(shopGoods): 优化商品列表和后端排序逻辑
- 商品列表「编号」列移除,改为名称下方灰色小字显示编号
- 市场价、价格、会员价三列合并为单列,按编辑弹窗顺序堆叠显示
- 后端新增 is_top 字段,实现置顶商品排序优先级调整
- pageRel 和 listRel 默认排序改为置顶优先、排序号次之、创建时间最后
- 采集默认设置 isTop 字段为 0,保持一致性
- 删除单独「编号」列节省列表横向空间,布局更紧凑
- 后端排序调整影响所有调用分页接口的端,置顶商品浮至最前
- 编辑弹窗「套餐价格」更名「进货价格」,对应接口字段注释同步修改
2026-07-24 11:17:35 +08:00
gxwebsoft e2f6dce7a1 feat(shopGoods): 新增商品置顶功能及批量上下架操作
- 在商品模型中新增 isTop 字段支持置顶状态(1置顶,0不置顶)
- 商品列表新增置顶列,支持列表内联切换置顶状态
- 启用排序号列排序功能,置顶列参与前端排序
- 新增单个商品上架/下架切换接口及批量上下架操作
- 优化商品列表工具栏和选中操作栏,加入批量上架、下架按钮
- 商品分类及图片列表调用 OSS 图片压缩接口处理展示图片
- 新增删除商品导入模板及图片导入模板下载链接与使用说明
- 优化图片压缩工具,新增去除 URL 中 x-oss-process 参数功能,确保调用参数生效
2026-07-24 10:57:23 +08:00
gxwebsoft 44e416525d fix(shopOrder): 修复导出商品总数量和完善导出字段
- 新增 getGoodsTotalNum 函数,基于订单商品明细求总数量,修正商品总数量计算错误
- 修改导出商品总数量列,避免使用订单级 totalNum 字段,确保数量正确
- 补齐导出字段,从8列扩展至15列,包含收货人、联系电话、配送方式等物流关键字段
- 导出数据信息同步商品明细,通过 Promise.all 调用逐单获取商品详情
- 支持按当前标签筛选导出,确保导出数据符合当前视图筛选条件
- 增加辅助函数优化代码结构,如 getShopName、getDeliveryTypeLabel 等
2026-07-23 08:00:34 +08:00
gxwebsoft 708bc7fc80 feat(shopOrder): 补充订单商品明细显示并优化列表数据加载
- 引入订单商品明细接口listShopOrderGoods以获取详细商品数据
- 修改商品汇总显示,增加规格信息展示(商品名(规格) x 数量)
- 优化订单列表加载,针对每笔订单单独请求商品明细数据补全
- 解决订单列表接口不返回orderGoods字段的问题,保证商品明细完整显示
- 确保异步获取商品详情失败时,使用空数组避免页面错误
2026-07-23 07:56:19 +08:00
gxwebsoft a4107e212a feat(shopOrder): 按当前标签筛选状态导出订单
- 搜索组件增加 statusFilter 属性以支持导出时传入筛选状态
- 在 shopOrder 页面通过计算属性传递当前标签的筛选状态给搜索组件
- 导出时订单列表请求中带入当前标签对应的筛选状态,避免混入其它状态订单
- 优化导出功能的状态一致性,提升使用体验
2026-07-23 07:51:50 +08:00
gxwebsoft c9a4e4c229 feat(shopOrder): 优化订单导出数据格式与内容
- 新增配送方式、发货状态等字段的中文标签转换函数
- 修改导出表头,调整字段顺序及名称,使信息更完整
- 导出内容增加收货人、联系电话、收货地址、商品明细等详细信息
- 优化商品明细汇总,显示商品名及数量
- 区分自提与快递的发货店铺名称展示
- 设置导出Excel表格列宽,更加美观规整
- 增加配送时间、自提码、买家备注等字段导出内容
2026-07-23 07:45:35 +08:00
gxwebsoft f65b8a945d fix(login): 互换登录页右上角扫码与手机图标
- 调整扫码模式下显示手机图标,账号/手机模式显示二维码图标
- 解决图标显示与目标模式相反的问题
- 优化用户界面切换按钮的视觉表达
2026-07-18 19:43:54 +08:00
gxwebsoft 9395e63f20 feat(passport): 优化登录页账号/手机号切换和指示器动画
- 调整默认登录类型为账号登录,支持账号和手机号登录切换
- 添加底部蓝色下划线指示器,动态显示当前选中标签位置和宽度
- 实现指示器收缩滑动和展开的两阶段动画效果,提升交互体验
- 增加窗口尺寸变化时自动调整指示器位置和宽度
- 修改扫码登录切换逻辑,避免意外切换至手机号登录
- 在账号登录和手机号登录页底部添加提示“还没有账号?联系管理员开通”
2026-07-18 17:10:08 +08:00
gxwebsoft 2037b0699a feat(passport): 支持邮箱和用户ID登录合并账号登录
- LoginParam 接口新增 email 和 userId 字段
- 新增 loginByEmail 和 loginByUserId 接口调用,逻辑与 login 类似
- 登录页账号输入框改为 account,支持输入邮箱或用户ID
- 提交时根据账号格式(邮箱或纯数字)调用对应登录接口
- 校验规则改为校验 account 字段,格式不符时提示错误
- 手机号短信登录改用 loginBySuperAdminSms 接口替代 loginBySms
- 登录成功后保存租户ID和用户ID至 localStorage
- 优化登录页相关导入及逻辑,提升登录体验
2026-07-18 17:00:39 +08:00
gxwebsoft bef50c2f26 feat(order): 支持线下付款先发货后结款流程
- 后端移除确认收款时订单状态限制,已发货订单确认收款后自动完成订单
- 管理后台订单列表和操作按钮区分线下付款未付款不同发货状态
- 离线付款弹窗新增发货状态字段及提示文案动态展示
- 订单详情支付状态标签区分已发货待收款状态,更新步骤条显示逻辑
- 小程序端订单状态文本和操作按钮支持线下付款先发货后结款场景
2026-07-17 15:09:51 +08:00
gxwebsoft 57be388a65 fix(xinlong-shop-taro): 修复订单详情页配送方式判断逻辑
- 纠正了判断快递配送的条件,避免误判商家送货为快递配送
- 订单收货信息卡片显示不同配送方式对应的图标和门店名称
- 订单信息区新增配送方式字段,映射 DELIVERY_TYPE_MAP
- 规范了 deliveryType 的值范围及对应含义
- 修复时纠正了误改项目,将改动应用到商家端项目 xinlong-shop-taro
2026-07-17 12:10:10 +08:00
gxwebsoft bd814d00b0 feat(shop): 优化发货弹窗文案及修复小程序配送方式判断
- deliveryModal.vue 中发货门店、发货人标签及提示文案,根据快递配送与商家送货动态显示
- 快递配送场景下新增寄件人信息同步至快递面单的小字说明和验证文案动态调整
- 修正小程序订单详情页 deliveryType 判断,新增 DELIVERY_TYPE_MAP 支持快递及商家送货标识
- 订单详情页收货信息卡片图标和配送门店名称根据实际配送方式动态显示
- 订单信息区新增配送方式字段,清晰展示快递配送、自提及商家送货三种状态
2026-07-17 12:05:22 +08:00
gxwebsoft 3edbb13a8d fix(orders): 修正发货功能关联的项目错误导致 storeId 为 0
- 发现并纠正之前误操作在错误项目 guilixu-taro 上的改动
- 恢复 guilixu-taro 项目中误改的文件,删除新增文件
- 明确实际工作项目为 xinlong-shop-taro,该项目已内置发货功能
- 解决 xinlong-shop-taro 中 openShipModal 调用 listShopStoreUser 未传 storeId 导致默认 0 的问题
- 修改 openShipModal 先调用 getMyClerk 获取 storeId,再传递给 listShopStoreUser 拉取本门店店员
- 默认选中当前登录人对应的店员,完善发货弹窗功能
- 警示跨项目操作时需确认准确项目路径,避免混淆同名 API 和项目目录
2026-07-17 11:36:36 +08:00
gxwebsoft b406973039 feat(store): 修正 storeId 获取并新增发货人员选择功能
- 排查 orders 页请求 storeId=0 的根因,确认旧版 H5 构建发出该请求
- 明确门店 storeId 来源于 getMyClerk() 返回的 ShopStoreUser.storeId 字段
- 实现全局 useClerk 上下文,管理店员及门店信息,避免局部状态冗余
- 重构用户中心与门店中心页面为调用 useClerk,移除局部 storeInfo 状态
- 补全 ShopStoreUser 模型字段,支持完整店员信息展示
- 在 orders 页新增“确认发货”功能,支持选择发货人员并提交发货请求
- 发货单提交包含 sendStoreId 和 deliveryUserId,确保后端一致落库
- 全项目类型检查无新增报错,优化门店相关接口调用逻辑和状态管理
2026-07-17 11:31:42 +08:00
gxwebsoft 2745f4b0db feat(shopOrder): 增加发货门店与配送店员选择功能
- 在发货类型非“无需发货”时新增发货门店下拉选择,支持加载门店列表
- 选择门店后自动回填发货地址及快递配送发货人信息
- 商家送货时新增配送店员下拉筛选,支持根据门店联动加载配送人员
- 配送店员选择后自动回填发货人姓名和电话
- 表单校验新增发货门店必填及配送店员在商家送货时必填规则
- 更新提交数据结构,包含发货门店ID与配送店员ID字段
- 订单详情页增加发货门店及配送店员展示字段
- 调整配送方式单选按钮顺序,快递配送与商家送货前置,无需发货后置
2026-07-17 04:17:57 +08:00
gxwebsoft e9249c910f feat(shopOrder): 增加发货门店与配送店员选择功能
- 在发货类型非“无需发货”时新增发货门店下拉选择,支持加载门店列表
- 选择门店后自动回填发货地址及快递配送发货人信息
- 商家送货时新增配送店员下拉筛选,支持根据门店联动加载配送人员
- 配送店员选择后自动回填发货人姓名和电话
- 表单校验新增发货门店必填及配送店员在商家送货时必填规则
- 更新提交数据结构,包含发货门店ID与配送店员ID字段
- 订单详情页增加发货门店及配送店员展示字段
- 调整配送方式单选按钮顺序,快递配送与商家送货前置,无需发货后置
2026-07-17 04:11:45 +08:00
gxwebsoft 6b57dd2fed fix(shopOrder): 修正确认收货状态码
- 将 deliveryStatus 状态码由 30 修改为 20,保持与已收货状态一致
- 确认收货操作后订单状态设置为已完成
- 更新相关组件中确认收货的逻辑状态码展示
2026-07-17 00:05:17 +08:00
gxwebsoft c9e074366f =========== V3.0.1 ==========
- 稳定版
- 可以基于此版本作为其他项目的底座
2026-07-16 22:16:53 +08:00
gxwebsoft b07ceaa8f1 =========== V3.0.1 ========== 2026-07-16 22:08:07 +08:00
gxwebsoft 1662daf436 fix(dashboard): 修复刷新统计数据未更新问题
- 交换基本信息和今日数据概况面板位置,基本信息放左侧,今日数据概况放右侧
- 修改刷新统计数据方法调用,强制传入参数跳过缓存
- 删除测试通知按钮,简化界面
- 确保刷新数据按钮可用且显示加载状态
- 添加刷新成功提示,增强用户体验
2026-07-16 21:32:19 +08:00
gxwebsoft 1d69923e06 refactor(shop-dashboard): 调整商城后台Dashboard布局及面板结构
- 基本信息面板改为左右两列并排显示,响应式列数调整为2列
- 今日数据概况与基本信息合并至同一行,分别占用半宽空间
- 今日数据概况面板栅格调整为2列,样式和间距优化
- 删除快捷操作面板及相关图标导入和缓存清理功能
- 快速入口功能调整为动态渲染卡片布局取代原按键列表
- 优化部分样式,如待办任务列表的内边距和字体大小调整
- 头像显示增加默认图片占位,避免空白显示
2026-07-16 21:25:22 +08:00
gxwebsoft 0b6563aad9 refactor(cms): 更新商城名称及相关展示文案
- 将网页标题由“企业商城”修改为“小程序商城”
- 修改仪表盘技术支持项显示名称为“企业官网”
- 更新登录页默认配置中站点名称为“小程序商城”
2026-07-16 14:13:33 +08:00
gxwebsoft 6c50ffa58b refactor(cms): 更新商城名称及相关展示文案
- 将网页标题由“企业商城”修改为“小程序商城”
- 修改仪表盘技术支持项显示名称为“企业官网”
- 更新登录页默认配置中站点名称为“小程序商城”
2026-07-16 14:10:23 +08:00
gxwebsoft 6fcc120e91 fix(subscription): 修复续费配置查询被租户隔离导致问题
- 修正 `MybatisPlusConfig.java` 中租户拦截器忽略表列表,新增 `"app_setting"` 以防止租户隔离导致配置查询失败
- 解决续费时 `platform_miniprogram` 配置查询因租户过滤返回空的问题,从而避免报“配置不存在或解析失败”
- 复核 `app_setting` 表查询应无租户隔离,与代码注释设计意图保持一致
- 记录排查方法,使用 pymysql 连接远程数据库验证数据状态
- 纠正广告编辑保存逻辑,取消广告图片 URL 拼接 OSS 压缩处理后缀
- 修改 cmsAd 组件 `chooseFile` 函数,图片 URL 直接使用原始链接,不再添加图片压缩参数
- 保持其他 cms 组件未改动,根据用户需求仅调整广告相关逻辑
2026-07-15 22:51:47 +08:00
gxwebsoft 015cd50c08 feat(subscription): 对齐续费接口为一步生成支付码
- 修正续费请求路径为后端实际的 /renew-pay/{id},避免 404 错误
- 续费接口新增 method 和 envVersion 参数支持余额与微信支付
- 续费请求返回支付结果,微信支付返回小程序码,余额支付直接成功
- 续费不再分步调起支付,前端确认续费逻辑简化为一步调用后端接口
- 后端 renewPay 优化,避免放弃支付导致服务中断及时长丢失
- 后端续费激活逻辑调整,续费保留原订阅有效期累计时长
- 排查并解决微信小程序支付配置缺失导致续费失败的问题
- 代码及注释同步更新,完善续费流程整体一致性与稳定性
2026-07-15 22:19:54 +08:00
gxwebsoft 5b5bb3b0e5 style(shopGoodsBrowse): 移除商品名称列的居中对齐
- 删除了商品名称列中多余的 align: 'center' 属性
- 保持了 ellipsis 属性以支持文本省略显示
- 优化了表格列的展示样式一致性
2026-07-15 18:55:37 +08:00
gxwebsoft ace46ffea6 style(shopGoodsBrowse): 移除商品名称列的居中对齐
- 删除了商品名称列中多余的 align: 'center' 属性
- 保持了 ellipsis 属性以支持文本省略显示
- 优化了表格列的展示样式一致性
2026-07-15 17:07:32 +08:00
gxwebsoft a2c4f49ee6 refactor(dashboard): 重构基本信息面板数据源及运行天数计算
- 统一从 app_product.product_id=65 和 app_subscription 获取面板数据,替代原硬编码和 tenant/siteStore
- 字段映射调整,软件名称、版本号、运行状态、创建时间等均改用 product 与 subscription 字段
- 删除硬编码的 systemInfo reactive,移除不必要的 reactive 导入
- 新增 productStatusText 计算属性,对 publishStatus 状态做文字和颜色映射
- 重写运行天数计算,改用订阅生效时间 startTime,修正续费重置计数的边界响应
- 模板绑定全部字段,增加骨架屏 loading 状态,避免界面闪烁
- 添加「系统运行」字段提示信息,明确起算时间语义
- 保持 loadSubscriptionInfo 调用 getProductDetail,无新增接口请求
- 确保 vue-tsc 类型校验无新增错误
2026-07-15 16:52:43 +08:00
gxwebsoft 00346d96d8 feat(shop-dashboard): 优化系统信息展示及状态显示
- 使用骨架屏组件优化系统名称、版本号、创建时间、系统运行天数等信息加载体验
- 引入发布状态映射,使用标签显示应用运行状态,提升状态直观性
- 分离系统信息数据源,区分身份信息和时间线数据来源
- 实现系统运行天数计算,支持基于订阅起始时间动态展示
- shopOrder组件调整支付凭证图片尺寸及位置,优化界面布局
- shopDealerWithdrawEdit组件新增paymentVoucher字段,支持支付凭证信息录入
2026-07-15 16:37:55 +08:00
gxwebsoft 071a3bf110 fix(shopOrder): 默认tab改为全部,订单详情页增支付凭证图
- 将 shopOrder 选项卡默认值从 'undelivered' 改为 'all',确保首页展示全部订单数据
- 修正初次加载时因缺少 statusFilter 导致数据不符的问题
- 订单详情页新增“支付凭证”显示,支持线下付款上传凭证图片预览
- 引入确保完整URL显示的工具函数 ensureFullUrl,配合压缩图像展示
- form reactive 新增 paymentVoucher 字段,符合 ShopOrder 模型定义
- 仅线下付款订单显示该凭证图,条件渲染自然处理
2026-07-15 15:53:14 +08:00
gxwebsoft d5e040b1e6 feat(shopOrder): 实现电脑版后台全局新订单红点提示
- 新建模块级全局单例 useOrderNotify,统一轮询检测新订单,避免多组件重复请求
- 轮询检测到新订单时触发叮声、语音播报,通知所有已注册回调刷新数据
- 订单管理菜单及 dashboard 快速入口显示红点提示新订单未查看
- 进入订单列表页时调用 clearBadge 同步最新订单ID并清除红点,标记订单已查看
- layout 全局启动新订单提醒,任意页面均生效
- menu-title 组件新增纯红点(dot)徽章显示支持
- dashboard 快速入口图标加红点显示及脉冲动画,提升视觉提示效果
- 维护新订单提醒回调集合,组件卸载自动移除回调避免内存泄漏
- 新订单提醒配置项和状态均为模块共享响应式,支持多组件无冲突使用
2026-07-15 15:26:48 +08:00
gxwebsoft 3a951c70f5 feat(shop): 列表商品图片改用 OSS 压缩函数
- src/views/shop/shopGoodsBrowse/index.vue 中引入 getCompressedImageUrl
- 商品图片 :src 改为使用 getCompressedImageUrl(record.goodsImage)
- 统一默认宽度 240px,质量 90,自动补全 OSS 域名
- 跳过已含 x-oss-process 的 URL 保持预览行为不变
2026-07-15 14:50:22 +08:00
gxwebsoft 2abd7e3372 fix(shop): 修正订阅流程及产品价格单位处理
- 后端 generatePayQrcode 价格计算改为 price/100 转元,年付按 10 个月计价
- 前端 dashboard 订阅价格同步调整,保持与后端一致的单位换算
- 解决 generatePayQrcode price_type 参数无生效导致的报错问题
- confirmSubscribe 改用 subscribe + pay 两步流程,规避后端未重启的问题
- 移除 confirmSubscribe 中的 isFreeProduct 分支,改用订阅状态判断
- 订阅信息加载策略调整,固定使用 productId=65 查询产品详情
- 保证前端代码通过 vue-tsc 校验,未引入新 TS 错误
2026-07-15 14:27:47 +08:00
gxwebsoft 7c73f9f821 feat(subscription): 优化订阅及支付二维码生成流程
- 统一调用 subscribe 接口创建订阅,确保后端设置价格类型
- 免费应用订阅成功后直接激活,提示订阅成功并刷新订阅信息
- 付费应用订阅成功后调用 pay 接口生成支付小程序码
- 支持显示支付二维码,展示付费信息,弹出支付模态框
- 增加支付状态轮询逻辑以跟踪支付进度
- 优化错误处理,打印错误日志并提示用户重试
2026-07-15 14:25:14 +08:00
gxwebsoft 09af76e4ea fix(subscription): 修复价格单位错误导致金额多100倍问题
- 确认 product.price 实际单位为分,修正所有价格计算需除以100转为元
- 后端 generatePayQrcode 方法价格计算改为 price/100,年付按10个月计费,季度付按3个月计费
- 前端 dashboard/index.vue 月付价格除以100,年付价格按月付价乘以10计算,保持与后端一致
- 保持 subscribe 价格计算逻辑,避免价格单位不一致的错误
- 修复后价格显示和支付逻辑统一,解决用户反馈金额偏大问题
2026-07-15 14:12:54 +08:00
gxwebsoft 29a18ffbc6 feat(appSubscription): 实现Dashboard订阅到期显示及扫码支付功能
- dashboard订阅到期时间信息支持三态渲染:激活显示剩余天数和已激活标签、待支付显示金额及去支付按钮、无订阅显示立即订阅按钮
- 通过租户ID查询应用产品,实现按租户过滤的订阅展示逻辑
- 新增appProduct分页接口及类型定义,提供应用列表获取能力
- 实现「立即订阅」流程,支持免费应用直接激活,付费应用生成微信扫码支付二维码及轮询订单状态
- 实现待支付订阅复用支付接口生成支付二维码,避免重复创建订阅
- 小程序端扫码支付流程支持订单号查询及支付确认,后端使用Redis标记支付成功状态
- 修复后端generatePayQrcode接口缺少priceType字段导致的数据库报错,前端兼容现有逻辑无需修改
- 移除dashboard中siteInfo旧的expirationTime字段相关代码,改为读订阅expireTime
- 新增订阅及支付相关样式,优化界面体验
- 支付状态轮询实现超时提示及自动停止机制,提升用户支付体验
- 组件卸载时确保支付轮询停止,避免内存泄漏
- 独立加载订阅信息,不影响其他数据请求,避免页面加载失败影响订阅展示
2026-07-15 14:11:14 +08:00
gxwebsoft e0d27f67d5 feat(shopOrder): 新增确认线下收款上传支付凭证功能
- 重构确认线下收款为独立组件 OfflinePaymentModal.vue
- 新增支付凭证字段 paymentVoucher,支持图片上传与预览
- 上传限制为 jpg/png 格式,单张图片最大 10MB
- API confirmOfflinePayment 新增 paymentVoucher 参数
- 页面移除原 Modal.confirm 逻辑,改为调用新组件弹窗
- 修复 buyerRemarks 和 merchantRemarks 类型错误
- 后端 shop_order 表待新增 payment_voucher 字段和接口支持
- 同步更新相关接口模型与视图交互逻辑

feat(shopGoodsBrowse): 新增用户商品浏览记录管理功能

- 新增 ShopGoodsBrowse 接口模型及分页查询、删除接口
- 前端新增浏览记录管理页面,实现列表展示和删除
- 支持基于用户ID、商品ID、浏览来源和日期范围搜索
- 列表显示商品图片、名称、价格、浏览次数及时间等信息
- 添加图片预览和操作确认弹窗,保障操作体验和安全
2026-07-15 13:37:27 +08:00
gxwebsoft a59d428e78 feat(shopOrder): 新增确认线下收款上传支付凭证功能
- 重构确认线下收款为独立组件 OfflinePaymentModal.vue
- 新增支付凭证字段 paymentVoucher,支持图片上传与预览
- 上传限制为 jpg/png 格式,单张图片最大 10MB
- API confirmOfflinePayment 新增 paymentVoucher 参数
- 页面移除原 Modal.confirm 逻辑,改为调用新组件弹窗
- 修复 buyerRemarks 和 merchantRemarks 类型错误
- 后端 shop_order 表待新增 payment_voucher 字段和接口支持
- 同步更新相关接口模型与视图交互逻辑

feat(shopGoodsBrowse): 新增用户商品浏览记录管理功能

- 新增 ShopGoodsBrowse 接口模型及分页查询、删除接口
- 前端新增浏览记录管理页面,实现列表展示和删除
- 支持基于用户ID、商品ID、浏览来源和日期范围搜索
- 列表显示商品图片、名称、价格、浏览次数及时间等信息
- 添加图片预览和操作确认弹窗,保障操作体验和安全
2026-07-15 13:30:14 +08:00
gxwebsoft 9e65007e65 feat(appSubscription): 新增应用订阅模块及商城欢迎横幅Logo支持
- 新增 useAppSubscriptionStore,封装 websopy 特殊接口的应用订阅功能
- 实现订阅列表分页查询、详情获取、支付状态查询、订阅管理等接口调用
- 适配不同支付方式,支持余额支付、微信支付及小程序支付预支付流程
- 在商城 Dashboard 欢迎横幅左侧新增商城 Logo 显示,读取商城设置的 shopLogo 字段
- 使用图像压缩接口处理 Logo 大小和质量,优化加载体验
- 优化 Dashboard 待处理事项展示,恢复退款和优惠券使用项
- 统一订阅接口调用地址为独立域名,复用请求工具共享认证和错误处理
2026-07-15 12:49:05 +08:00
gxwebsoft e2dd8dbc18 feat(cms): 更新系统名称并优化租户信息获取逻辑
- 将系统名称由“小程序开发”修改为“小程序商城”
- 修改多处组件中系统名称的引用,统一使用系统信息对象
- 调整快捷入口路由,更新优惠券和会员管理的跳转路径
- 使用 tenantStore 替代原有的接口调用,统一获取租户公司信息
- 优化租户创建时间的计算方式,直接从 store 获取数据
- 移除不再使用的 getTenantInfo 方法,改为通过 pinia 状态管理获取
- 调整部分模板数据绑定,修正创建时间显示逻辑
2026-07-15 12:04:34 +08:00
gxwebsoft 08b697556b feat(cms): 更新系统名称并优化租户信息获取逻辑
- 将系统名称由“小程序开发”修改为“小程序商城”
- 修改多处组件中系统名称的引用,统一使用系统信息对象
- 调整快捷入口路由,更新优惠券和会员管理的跳转路径
- 使用 tenantStore 替代原有的接口调用,统一获取租户公司信息
- 优化租户创建时间的计算方式,直接从 store 获取数据
- 移除不再使用的 getTenantInfo 方法,改为通过 pinia 状态管理获取
- 调整部分模板数据绑定,修正创建时间显示逻辑
2026-07-15 12:00:14 +08:00
gxwebsoft b0ff82599a feat(shop): 新增Dashboard待付款订单统计及支持
- Dashboard待处理事项新增“待付款订单”项,使用statusFilter=0查询
- 点击待付款订单跳转至/shop/shopOrder?tab=unpaid页面
- shopOrder页面恢复“待付款”标签页,validTabs列表包含unpaid
- shopOrder页面根据路由query初始化活动标签,支持unpaid选项
- 新增dot-gold样式用于待付款订单高亮显示
- 订单数量接口新增待付款订单数获取及异常处理逻辑
- 调整组件中待处理事项列表,支持待付款订单数据响应式更新
2026-07-15 03:46:23 +08:00
gxwebsoft 57b584df1b feat(shop): 支持Dashboard订单总数点击跳转指定tab
- 修改Dashboard中订单总数和总营业额跳转链接,默认携带tab参数
- 订单总数点击跳转至/shop/shopOrder?tab=all页面
- 待发货订单跳转至/shop/shopOrder?tab=undelivered,退款申请跳转至?tab=refunded
- shopOrder页面根据路由查询参数初始化激活tab,确保页面首次加载可正确显示对应订单列表
- 为无效或缺省tab参数设置默认值undelivered
- 简化订单页面tab切换逻辑复用公共状态映射函数
2026-07-15 02:35:01 +08:00
gxwebsoft 6350a9b5a9 fix(shopOrder): 调整商品头像显示尺寸并优化图片压缩
- 修改订单商品头像大小,从80调整为50
- 调整图片压缩函数调用,设置压缩宽度为100
- 修正商品信息组件中头像图片的压缩调用,统一宽度参数
- 引入图片压缩工具方法以支持新参数调用
2026-07-15 01:40:12 +08:00
gxwebsoft 614e843673 fix(shopOrder): 修复初次加载订单状态筛选问题
- 提取 getStatusFilterByTab 函数实现 tab key 与 statusFilter 的映射
- datasource 函数中当 statusFilter 未设置时,自动根据 activeKey 推导赋值
- onTabs 函数简化为复用 getStatusFilterByTab 函数,减少重复代码
- 确保初次加载时待发货等状态的 tab 能正确筛选订单数据
2026-07-15 01:29:44 +08:00
gxwebsoft 894592c290 feat(shop): 新增线下付款支持及优化Dashboard数据统计
- 新增线下付款(payType=9)支付方式,支持订单中标识和确认收款操作
- 实现商家确认线下付款后订单进入待发货状态功能
- 更新支付方式组件和订单列表及详情页的支付状态显示
- 优化shop/dashboard快捷操作按钮为商城常用功能,修正快速入口路由路径
- 修复Dashboard待发货订单数统计时未过滤商城订单导致数据不准的问题
- 修复Dashboard运行天数为0的异常,将租户信息请求独立处理,避免某些请求失败影响
- 修复statisticsStore中couponUsedCount与getter同名冲突,改名为safeCouponUsedCount并同步更新引用
2026-07-14 22:17:39 +08:00
144 changed files with 12898 additions and 2602 deletions
+1
View File
@@ -1,2 +1,3 @@
VITE_APP_NAME=小程序商城
VITE_API_URL=/api
VITE_CMS_API_URL=https://cms-api.websoft.top/api
+2 -1
View File
@@ -1,3 +1,4 @@
VITE_APP_NAME=后台管理系统
#VITE_API_URL=http://127.0.0.1:9200/api
VITE_API_URL=https://shop-api.websoft.top/api
VITE_API_URL=https://guilixu-api.websoft.top/api
VITE_CMS_API_URL=https://cms-api.websoft.top/api
+2 -1
View File
@@ -1,2 +1,3 @@
VITE_APP_NAME=后台管理系统
VITE_API_URL=https://shop-api.websoft.top/api
VITE_API_URL=https://guilixu-api.websoft.top/api
VITE_CMS_API_URL=https://cms-api.websoft.top/api
+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
dist.zip
.verify-build
.DS_Store
*.local
+24
View File
@@ -6,3 +6,27 @@
-`src/views/shop/dashboard/index.vue` 的欢迎横幅右侧添加新订单提醒开关栏(开关 + 测试按钮),检测到新订单后自动调用 loadData() 刷新 Dashboard 数据。
-`src/views/cms/dashboard/index.vue` 的概况卡片标题栏添加新订单提醒控件,同样检测到新订单后刷新数据。
- 提取了 CMS Dashboard 的 loadData 函数使其可复用。
## 更新 shop/dashboard 快捷操作按钮为商城常用功能
- 快捷操作按钮从原来的通用功能(参数配置/用户管理/站点管理/登录日志)改为商城系统常用功能:订单管理、商品管理、商品分类、优惠券管理、会员管理、商城设置、清除缓存。
- 路由路径统一使用 `/shop/shopXxx` 格式(与 `/shop/shopOrder` 等已确认路径一致),修复了快速入口中 `/shopGoods``/shop/shopGoods` 的路径不一致问题。
- 快速入口九宫格也同步改为商城相关入口。
- 图标导入更新:移除 UngroupOutlined/CalendarOutlined/UserOutlined/FileTextOutlined,新增 ShoppingCartOutlined/AppstoreOutlined/GiftOutlined/TeamOutlined/SettingOutlined。
## 修复 shop/dashboard 待发货订单数量统计不准确
- 根因:订单列表页 datasource 中 `where.type = 0`(只查商城订单),但 Dashboard 统计未传 `type: 0`,导致把预定订单/外卖(type=1)和会员卡订单(type=2)也计入。
- 修复:为 pendingShipmentCount 和 pendingRefundCount 查询都加上 `type: 0`
## 修复 Dashboard 运行天数为 0 的问题
- 根因:`getTenantInfo()` 被放在 `await Promise.all([...])` 之后,如果 `statisticsStore.fetchStatistics()` 抛异常(`couponUsedCount` 错误),`Promise.all` reject 后直接跳到 `catch``getTenantInfo` 根本不会执行,`tenantCreateTime` 永远是空值。
- 修复:在 `shop/dashboard/index.vue``cms/dashboard/index.vue``loadData` 中,把 `getTenantInfo()` 放到 `Promise.all` 外面,用 `.then` 独立执行,不受其他请求失败影响。
- 运行天数计算基于 `getTenantInfo()` 返回的 `Company.createTime`(租户创建时间)。
## 修复 statisticsStore couponUsedCount 冲突
- 根因:`couponUsedCount` 同时被定义成 state 属性和 getterPinia 中同名冲突导致 action 里 `this.couponUsedCount = ...` 报错:`'set' on proxy: trap returned falsish`
- 修复:将 getter 改名为 `safeCouponUsedCount``shop/dashboard/index.vue` 中同步更新引用(该引用当前被注释,但保持一致性)。
+279
View File
@@ -0,0 +1,279 @@
# 2026-07-15
## shopOrder 选项卡默认改为全部
- 需求:`/shop/shopOrder` 选项卡默认从"待发货"改为"全部"。
- 改动 `src/views/shop/shopOrder/index.vue``activeKey` 初始值在路由参数 `tab` 无效时的回退值由 `'undelivered'` 改为 `'all'`
- `datasource``getStatusFilterByTab('all')` 返回 `undefined`,即不传 `statusFilter`,查全部订单,符合预期。
## 修复 shopOrder 页面默认待发货tab数据不正确
- 根因:`activeKey` 默认为 `'undelivered'`,但 `datasource` 函数只设了 `where.type = 0`,没有设 `statusFilter``statusFilter` 只在用户点击 tab 触发 `onTabs` 时才设置。所以页面初次加载时查的是全部订单而非待发货订单。
- 修复:提取 `getStatusFilterByTab(key)` 公共映射函数,在 `datasource` 中当 `where.statusFilter` 未设置时从 `activeKey` 自动推导。`onTabs` 也简化为复用同一函数。
- 文件:`src/views/shop/shopOrder/index.vue`
## Dashboard 订单总数点击跳转支持指定 tab
- Dashboard 中"订单总数"/"总营业额"跳转链接改为 `/shop/shopOrder?tab=all`"待发货订单"改为 `?tab=undelivered`"退款申请"改为 `?tab=refunded`
- shopOrder 页面读取 `route.query.tab` 初始化 `activeKey`,无效值回退到 `'undelivered'`
- 文件:`src/views/shop/dashboard/index.vue``src/views/shop/shopOrder/index.vue`
## Dashboard 待处理事项新增"待付款订单"
- 在待发货订单上方新增"待付款订单"统计项,使用 `statusFilter=0` 查询,点击跳转 `/shop/shopOrder?tab=unpaid`
- shopOrder 页面取消注释"待付款"tab`validTabs` 加入 `unpaid`
- 新增 `dot-gold` 样式。
- 文件:`src/views/shop/dashboard/index.vue``src/views/shop/shopOrder/index.vue`
## Dashboard 引入 useTenantStore,创建时间读租户信息
- 引入 `useTenantStore``loadData` 中调用 `tenantStore.fetchTenantInfo()` 替代直接调用 `getTenantInfo()` API。
- 基本信息"创建时间"改为读取 `tenantStore.company?.createTime`
- 运行天数也改为从 `tenantStore.company?.createTime` 计算,移除独立的 `tenantCreateTime` ref。
- 文件:`src/views/shop/dashboard/index.vue`
## Dashboard 欢迎横幅新增商城Logo
- 通过 `getShopSettingCategoryValues('basic')` 获取商城设置,读取 `shopLogo` 字段。
- 使用 `getCompressedImageUrl(shopLogo, { width: 200, quality: 90 })` 压缩图片。
- 在 welcome-banner 左侧用 `a-avatar` (64px, square) 展示,无 Logo 时不显示。
- 文件:`src/views/shop/dashboard/index.vue`
## 新增 useAppSubscriptionStorewebsopy 应用订阅特殊接口)
- 新增 `src/api/app/appSubscription/model.ts`AppSubscription 实体及参数/返回类型,对照后端 `com.gxwebsoft.app.entity.AppSubscription`
- 新增 `src/api/app/appSubscription/index.ts`:封装 `AppSubscriptionController` 全部 14 个接口。
- 特殊接口域名 `https://websopy-api.websoft.top/api`(与项目主 API 域名不同),复用 `@/utils/request`(共享登录态 token、401 处理),传绝对 URL。
- 后端无 context-pathbaseURL=`https://websopy-api.websoft.top/api`,接口路径用 `/app/subscription/xxx`
- 后端 `BaseController` 成功 code=0`success(IPage)` 转成 `PageResult{list,count}`,与项目 `@/api` 的 PageResult 结构一致。
- 新增 `src/store/modules/appSubscription.ts``useAppSubscriptionStore`Pinia Options API 风格,对齐 site.ts/statistics.ts),聚合列表/详情/支付状态/订阅管理方法。列表/详情缓存5分钟;`checkStatus`/`checkPurchased` 实时请求;订阅/支付/管理类操作成功后调 `invalidateCache()` 失效缓存。
## 确认线下收款新增上传凭证功能
- 背景:原"确认线下收款"用 `Modal.confirm` + `createVNode` 动态拼一个 input,只有备注,没有上传凭证。用户需要加凭证上传。
- 字段决策:新增 `paymentVoucher?: string` 字段(不复用 comments/merchantRemarks,因为凭证是图片URL非文本)。顺带修复 `buyerRemarks`/`merchantRemarks` 类型 bug`undefined``string?`)。
- 重构为独立组件 `src/views/shop/shopOrder/components/OfflinePaymentModal.vue`
-`<a-modal>` + `<a-form>` + `<a-upload list-type="picture-card">` 单张图片上传。
- 上传走 `uploadOss``src/api/system/file`),存储返回的 `path`(与项目 `UploadCert` 组件约定一致),预览用 `getUrl(path)` 补全。
- 凭证必填,备注可选;校验 jpg/png、≤10MB;支持点击预览大图。
- API `confirmOfflinePayment(id, remarks?, paymentVoucher?)` 新增第三参数,通过 query params 传给后端。
- `index.vue`:移除原 `Modal.confirm`/`createVNode` 逻辑,改为打开新组件(`v-model:visible` + `:data="current"` + `@done="reload"`),与 `DeliveryModal` 模式一致。
- 类型检查:新增 0 错误,顺带消掉原 `okType: 'success'` 的类型错误(antdv4 ButtonType 不含 success,新组件去掉了 ok-type)。
- 改动文件:`src/api/shop/shopOrder/model/index.ts``src/api/shop/shopOrder/index.ts``src/views/shop/shopOrder/components/OfflinePaymentModal.vue`(新增)、`src/views/shop/shopOrder/index.vue`
- 后端待办(已完成):后端项目 `/Users/gxwebsoft/JAVA/guilixu-java`MyBatis-Plus 3.4.3.3,无 Flyway/LiquibaseDDL 手动管理,SQL 脚本放 `sql/` 目录):
- `ShopOrder.java` 实体新增 `paymentVoucher` 字段(payTime 之后)。
- `ShopOrderController.confirmOfflinePayment` 新增 `@RequestParam paymentVoucher`
- `ShopOrderService` 接口 + `ShopOrderServiceImpl` 实现方法签名加 `paymentVoucher` 参数,lambdaUpdate 加 `.set(paymentVoucher 非空, ShopOrder::getPaymentVoucher, paymentVoucher)`
- DDL 脚本 `sql/shop_order_payment_voucher.sql``ALTER TABLE shop_order ADD COLUMN payment_voucher VARCHAR(500) NULL ... AFTER pay_time`
- 注意:方法签名从 2 参变 3 参,全项目仅 Controller 一处调用,无断裂。
## Dashboard 到期时间显示订阅信息 + 立即订阅/扫码支付
- 改造 `src/views/shop/dashboard/index.vue` 基本信息「到期时间」行:三态渲染(active 显示 expireTime + 已激活 tag + ≤7天到期提醒;pending 显示待支付 + 去支付;无订阅显示立即订阅)。
- **productId 关联条件**`app_product.tenantId = userStore.info.tenantId`(非 siteInfo.appId)。通过 `pageProducts({tenantId,current:1,size:1})` 取该租户应用。
- 新增 `src/api/app/appProduct/{model.ts,index.ts}`AppProduct 类型 + `pageProducts` 接口(GET /api/app/product/page,分页参数 current/size,注意与项目 page/limit 不同)。
- 「立即订阅」流程:选 month/year → 免费应用调 `subscribe()` 直接激活;付费应用调 `generatePayQrcode()` 生成小程序码 → Modal 展示二维码 → 每 2.5s 轮询 `checkStatus(subscriptionNo)`,paid 后关闭+刷新(最多 5 分钟)。envVersion 按 `import.meta.env.DEV` 自动判断 trial/release。
- pending 订阅「去支付」复用 `subscriptionStore.pay(id,'wechat')`(用 `'miniappQrcode' in result` 类型守卫窄化)生成小程序码,不重复创建订阅。
- 小程序端支付页 `websopy-taro/src/passport/pay/index.tsx`:扫码 scene=subscriptionNo → detail-by-no → mp-prepay → requestPayment → mp-confirm,后端写 Redis `wxpay:paid:{no}=1`Web 端 checkStatus 据此感知。
- 移除 dashboard 不再使用的 `siteInfo` 解构(原到期时间读 `siteInfo.expirationTime`,已改为订阅 expireTime),修复 noUnusedLocals 报错。
## 修复后端 generatePayQrcode 报错 "Field 'price_type' doesn't have a default value"
- 现象:dashboard 点「立即订阅」付费应用调 `generatePayQrcode` 时,后端 insert app_subscription 报 `price_type` 无默认值。
- 根因:后端 `AppSubscriptionController.generatePayQrcode`websopy-java)创建订阅时漏了 `setPriceType()`,而 DB `price_type` 为 NOT NULL 无默认值。对比 `subscribe` 方法有 `setPriceType`
- 参考 `websopy-pc/app/pages/console/pay/[subscriptionNo].vue`:它用 `subscribe` 创建订阅 + `pay(id,'wechat')` 生成二维码(两步),避开了 `generatePayQrcode`,所以没遇到此 bug。
- 修复(后端 `AppSubscriptionController.java` generatePayQrcode 方法):补全 `setPriceType(product.getPriceType())` + `setOriginalPrice`/`setPayStatus(0)`/`setTenantId`,对齐 subscribe。**前端无需改动**,重启后端即可。
- 注意:`subscribe` 价格计算用 `price/100`(当分转元),`generatePayQrcode` 直接用 `price`(当元),两者不一致;schema 标注 price 为「元」,故 `generatePayQrcode` 价格逻辑更合理,保留不动。
## 修复价格多了100倍(product.price 实际单位是分)
- 用户反馈:`generatePayQrcode` 算出的金额多了100倍。确认 `product.price` 实际存的是**分**(非 schema 标注的元),故 `subscribe``/100` 是对的,`generatePayQrcode` 直接用 price 是错的。
- 后端 `AppSubscriptionController.generatePayQrcode` 价格计算改为 `price/100` 转元(对齐 subscribe),年付按10个月(*10,对齐 subscribe 年付优惠),季付*3,月付*1。
- 前端 `dashboard/index.vue` 参考价同步修正:`monthPrice = price/100``yearPrice = monthPrice*10`(原为 price 直接 + *12)。
- 结论:product.price 实际单位是**分**,所有价格展示/计算都需 /100 转元。
## 改用 subscribe+pay 避开 generatePayQrcode 的 price_type 报错
- 现象:用户反馈 `generatePayQrcode` 仍报 `price_type` 无默认值(报错 SQL 字段集是修复前的,说明后端未重启/未重编译,`setPriceType` 修复未生效)。
- 关键认知:`generatePayQrcode` 后端**不接收 priceType 参数**(从 `product.getPriceType()` 取),所以前端传 priceType 给它也没用,无法绕过。
- 修复(前端方案,不依赖后端重启):dashboard `confirmSubscribe` 改用 `subscribe` + `pay` 两步(参考 websopy-pc 的 `pay/[subscriptionNo].vue` 模式):
- `subscribe` 创建订阅(后端有 `setPriceType`,不报错;价格 `/100` 正确)→ 免费 `status=active` 直接成功
- 付费 `status=pending``pay(subscriptionId,'wechat')` 生成小程序码 → Modal 展示 + 轮询 `checkStatus`
- 移除 `confirmSubscribe` 里的 `isFreeProduct` 分支(改用 `subResult.status` 判断);`isFreeProduct` 仍用于订阅 Modal 的免费提示。
- vue-tsc 验证 dashboard 零 error TS(总错误 1778 不变)。
- 后端 `generatePayQrcode``setPriceType` 修复仍保留(重启后端后该接口也能用),但前端已不依赖它。
## 订阅 productId 改为固定 65(小程序商城)
- 需求:dashboard 立即订阅请求的 productId 从动态(按 `tenantId``app_product`)改为写死 `65`
- 改动 `src/views/shop/dashboard/index.vue`
- imports`pageProducts``getProductDetail`
- `loadSubscriptionInfo`:移除 `userStore.info.tenantId` 判断和 `pageProducts({tenantId,current:1,size:1})` 调用,改为 `getProductDetail(65)` 查固定产品详情;定义 `FIXED_PRODUCT_ID = 65` 常量,订阅列表过滤和 `currentProduct.productId` 统一使用该常量。
- `confirmSubscribe` 无需改动(用 `currentProduct.productId`,已自动是 65)。
- `getProductDetail(65)` 返回的 product 含 name/priceType/price,前端参考价 `monthPrice = price/100``yearPrice = monthPrice*10` 仍适用。
- vue-tsc 验证 dashboard 零 error TS(总错误 1778 不变)。
## shopGoodsBrowse 列表商品图片改用 OSS 压缩函数
- 文件:`src/views/shop/shopGoodsBrowse/index.vue`
- 引入 `getCompressedImageUrl`(来自 `src/utils/image.ts`),将商品图片 `:src="record.goodsImage"` 改为 `:src="getCompressedImageUrl(record.goodsImage)"`
-`shopGoods``shopOrder``dashboard` 等页面保持一致:默认宽度 240px、质量 90、自动补全 OSS 域名、跳过已含 `x-oss-process` 的 URL。
- 预览行为未改(仍走 a-image 默认 src 预览)。
## 电脑版后台新订单红点提示(全局)
- 需求:电脑版后台有新订单时显示红点,让管理员知道有新订单。
- 关键发现:系统已有菜单红点基础设施(`menu-title.vue``item.meta.badge` 渲染徽章;`user.ts` `setMenuBadge(path,value,color)``layout/index.vue``.ele-menu-badge` 样式),但 `useOrderNotify` 原为组件级(dashboard/cms dashboard/shopOrder 三处各自独立轮询),未接通红点。
- 改造 `src/views/shop/shopOrder/useOrderNotify.ts` 为**模块级全局单例**
- 轮询/状态/红点全部模块级变量(enabled、hasNewOrder、lastOrderId、timer),多组件调用只一个轮询。
- 检测到新订单 → triggerNotify(叮声+语音+遍历回调Set+ `setOrderBadge(true)``userStore.setMenuBadge('/shop/shopOrder','dot','#ff4d4f')`
- 新增 `clearBadge()`:立即清红点 + 重查最新订单同步 lastOrderId(标记"已查看")。
- `onNewOrder` 回调用 Set 管理,组件 onBeforeUnmount 自动移除;`start()` 幂等,由 layout 调一次。
- `src/layout/components/menu-title.vue``badge==='dot'` 时用 `<a-badge dot />` 纯红点,否则原数字徽章逻辑。
- `src/layout/index.vue`setup 调 `useOrderNotify().start()` 全局启动,后台任意页面生效。
- `src/views/shop/shopOrder/index.vue``onMounted` + `onActivated``clearBadge()`(兼容 keep-alive `cache-key`,用 `orderBadgeMounted` 标志防首次双触发)。
- `src/views/shop/dashboard/index.vue``quickLinks` 改 computed,订单管理项 `badge: hasNewOrder.value``quick-icon` 加红点 span + pulse 动画样式。
- 决策(用户确认):红点=新订单未查看(进订单页清除);位置=订单管理菜单+dashboard快速入口订单管理图标;声音保留并全局化。
- 构建验证:`vite build --outDir dist_verify` 通过(EXIT:0)。原 `pnpm build` 失败仅因 vite 清空 `dist/assets` 被安全删除保护拦截(83文件>50阈值),与代码无关。
## shopOrder 订单详情页(orderInfo.vue)支付方式下增加支付凭证图
- 背景:之前 `OfflinePaymentModal` 确认线下收款时已支持上传 `paymentVoucher`(OSS path),但订单详情页只展示「线下付款」tag,看不到凭证图。
- 改动 `src/views/shop/shopOrder/components/orderInfo.vue`
- 模板:在「支付方式」`<a-descriptions-item>` 之后、「开票状态」之前新增「支付凭证」项,`v-if="form.paymentVoucher"``:span="3"` 占满整行。
- 缩略图用 `a-image``:width="120"``getCompressedImageUrl(form.paymentVoucher, { width: 750 })`(按用户要求宽度 750),`preview-src-list``ensureFullUrl(form.paymentVoucher)` 给原图预览。
- 引入 `ensureFullUrl` 来自 `@/utils/image`(已有 `getCompressedImageUrl`)。
- form reactive 默认值新增 `paymentVoucher: undefined`,对齐 `ShopOrder` model。
- `ShopOrder` model 中 `paymentVoucher?: string` 字段早就存在(payTime 之后),无需改 model/API。
- 用户场景:仅线下付款(payType=9)的订单才有此凭证图,条件渲染自然处理。
## Dashboard 基本信息 6 字段数据源重构(product + subscription
- 需求:dashboard「基本信息」面板原先混用硬编码/tenant/siteStore,改为统一从 `app_product.product_id=65` + `app_subscription` 读取,语义更准确。
- 字段映射决策(用户确认):
- 系统名称 → `currentProduct.productName`(原硬编码 '小程序商城'
- 版本号 → `currentProduct.version`(原硬编码 '2.0.0'
- 运行状态 → `currentProduct.publishStatus` + 中文映射 + a-tag 颜色(原 `siteStore.statusText`
- 创建时间 → `currentSubscription.startTime`(原 `tenantStore.company?.createTime`
- 到期时间 → `currentSubscription.expireTime`(保持不变,原本就是这个)
- 系统运行天数 → 自 `currentSubscription.startTime` 起累计(原自 `tenantStore.company?.createTime`;后改为 startTime,与"创建时间"行同源,语义统一)
- 改动 `src/views/shop/dashboard/index.vue`
- 删除硬编码 `systemInfo` reactive(已确认 cms 那两个 systemInfo 是独立 const,互不影响)。
- import 移除 `reactive`
- 新增 `productStatusText` computedpublishStatus → {text,color} 映射(published=已发布/green、pending_review=审核中/orange、developing=开发中/blue、rejected=已驳回/red、deprecated=已下架/red),未知状态显示原始值,无 product 显示 '-'。
- 重写 `runDays` computed:数据源由 `tenantStore.company?.createTime` 改为 `currentSubscription.value?.startTime`(与"创建时间"行同源),注释说明续费新增订阅记录会重置计数的边界行为。
- 模板 6 个字段全部重绑,每个字段加 `subscriptionLoading` 骨架屏分支(避免空白/老数据闪现)。
- 「系统运行」加 `a-tooltip title="自您首次开通小程序商城起"`(用户要求)。
- 数据流复用:`loadSubscriptionInfo()` 原本就调 `getProductDetail(65)` 填充 `currentProduct`,无需新增接口调用。
- 类型校验:vue-tsc 对 `shop/dashboard/index.vue` 行级错误 0 条;cms 那批历史错误与本次无关。
## Dashboard 到期时间续费功能 + 标签矛盾修复
- 背景:dashboard「到期时间」行原先在订阅已过期时同时显示绿色「已激活」标签 + 「(已过期)」红字,状态自相矛盾;且即将到期/已过期时无续费入口(用户反馈"没有按钮呢")。
- 改动 `src/views/shop/dashboard/index.vue`
- 标签动态化:`a-tag :color="isExpired ? 'red' : 'green'"`,文案 `已过期/已激活`;已过期时显示「(已过期N天)」。
- 新增续费按钮:`showRenewButton``daysToExpire <= 7` 即即将到期或已过期时显示),点击 `onRenew`
- 复用订阅 Modal:新增 `subscribeMode`'subscribe'|'renew'+ `renewing` 状态,Modal 的 title/okText/confirm-loading/@ok 按 mode 动态切换;免费应用 alert 文案也按 mode 区分。
- `confirmRenew``subscriptionStore.renew(sub.id, selectedPeriod)`POST /app/subscription/renew/{id}?period=month|year),成功后 `refreshSubscription()`
- `onSubscribe` 补设 `subscribeMode='subscribe'`
- 续费 API 语义:`renewSubscription` 返回 string(同步完成,疑似余额扣款/免费续期)。若后端续费需走微信扫码支付,需改为返回支付二维码结构并接 pay 流程——待后端确认。
- 关键字段再确认(用户):开始时间用 `appSubscription.startTime`,到期时间用 `appSubscription.expireTime`daysToExpire 与 `dayjs()`(浏览器本地时间)对比。
## Dashboard 续费改走 subscribe+pay 扫码(后端 renew 接口未实现)
- 触发:用户截图显示 `POST /api/app/subscription/renew/90` 返回 404,确认后端没有实现 renew 接口。
- 方案:续费不再调 `store.renew` / `renewSubscription`,改为复用现有 `subscribe + pay` 扫码支付链路(与首次订阅完全一致)。后端 subscribe 对已有 active 订阅会处理续期/创建新记录。
- 改动 `src/views/shop/dashboard/index.vue``confirmRenew`
- 删掉 `await subscriptionStore.renew(sub.id, ...)` 改为 `subscribe({ productId, subscriptionPeriod })`
- 免费应用走 `subResult.status === 'active'` 分支,提示「续费成功」并刷新。
- 付费应用走 `pay(subscriptionId, 'wechat', envVersion)` → 拿到 `miniappQrcode` → 弹支付 Modal + `startPolling`
- `payProductName` fallback 文案改为「应用续费」(与订阅的「应用订阅」区分)。
- `onRenew` 入口、`subscribeMode='renew'`、续费 Modal title/按钮复用不变,UI 上用户感知不到底层调的是 subscribe。
- 教训/记录:调后端接口前最好先确认接口是否真实存在;前端 `api/app/appSubscription/index.ts``renewSubscription` 函数可保留(防止后端后续补上接口),但 dashboard 暂不调用。
## Dashboard 续费最终定稿:方案1 renew+pay(前端已就绪,等后端实现)
- 触发:上一步用 subscribe 走续费,后端返回「您已订阅该应用,无需重复购买」——subscribe 对已有 active 订阅是拒绝的,续费不能复用 subscribe。
- 定稿方案(用户确认):方案1 —— 后端实现 `/renew/{id}?period=xxx` 返回 `SubscribeResult`(含 subscriptionId),前端再调 `pay` 生成小程序码。与 subscribe+pay 对称,职责清晰。
- 三处改动(前端已全部就绪,等后端补 renew 接口即可联调通):
1. `src/api/app/appSubscription/index.ts` `renewSubscription`:返回类型 `string``SubscribeResult`;取 `res.data.data`(原取 `res.data.message`)。
2. `src/store/modules/appSubscription.ts` `renew`:返回类型 `string``SubscribeResult`,返回 result 而非 msg。
3. `src/views/shop/dashboard/index.vue` `confirmRenew`:改回调 `subscriptionStore.renew(sub.id, period)``renewResult` → 免费(status=active)直接成功;付费走 `pay(renewResult.subscriptionId, 'wechat', envVersion)` → 弹码 + `startPolling`。与 confirmSubscribe 完全对称,仅第一步 subscribe→renew、传 sub.id 而非 productId。
- **后端接口约定(待实现)**`POST /api/app/subscription/renew/{id}?period=month|year`
- 入参:id=原订阅IDperiod=month/year
- 逻辑:校验原订阅归属 → 创建 pending 续费记录(startTime=原expireTime或nowexpireTime=新到期)→ 返回 `{ subscriptionId, subscriptionNo, status, message, payPrice?, orderNo? }`(同 SubscribeResult
- 前端拿到后调 `pay/{subscriptionId}` 生成微信小程序码,与订阅流程一致
- 续费按钮触发条件不变:`showRenewButton`daysToExpire<=7,即将到期或已过期)。
## 续费最终对齐:后端已有 /renew-pay(路径不匹配导致 404,改前端即可)
- 关键发现:后端 `AppSubscriptionController` 其实**已经实现了续费**,方法是 `renewPay`line 849-910),路径是 `/api/app/subscription/renew-pay/{id}`(带连字符),不是前端调的 `/renew/{id}`,所以之前 404。
- 后端 renew-pay 逻辑(已实现,无需改动):
- 校验订阅存在 + priceType=subscription + 产品存在
- 计算续费价格(年付按10个月,对齐 subscribe
- 计算新到期:`baseTime = max(原expireTime, now)``newExpireTime = baseTime + period`(未过期从原到期延后,已过期从现在延后,不丢时长)
- 原订阅状态改 pending、覆盖 payPrice/period/expireTime
- method=balance → handleBalancePay(直接扣款激活);method=wechat → handleWechatPay(返回 miniappQrcode
- 前端三处改动(对齐后端,从方案1改为方案2"renew一步返码"):
1. `api/.../index.ts` `renewSubscription`:路径 `/renew/{id}``/renew-pay/{id}`;参数加 `method/envVersion`;返回类型 `SubscribeResult``PayResult | WechatNativePayResult`
2. `store/.../appSubscription.ts` `renew`:参数加 `method/envVersion`;返回类型 `PayResult | WechatNativePayResult`
3. `dashboard/index.vue` `confirmRenew``renew(sub.id, period, 'wechat', envVersion)` 直接拿支付结果 → `'paid' in result` 余额成功分支;`'miniappQrcode' in result` 弹码+轮询。不再二次调 pay。
- 教训:前端 404 时应先 grep 后端 Controller 确认接口是否真实存在及准确路径(带连字符的 `/renew-pay` 容易被误写为 `/renew`)。后端项目位于 `/Users/gxwebsoft/JAVA/websopy-java/`Controller 全路径 `com.gxwebsoft.app.controller.AppSubscriptionController`
## 续费报错"微信小程序配置不存在或解析失败"根因排查
- 报错:`POST /renew-pay/90` 返回"生成小程序码失败: 微信小程序配置不存在或解析失败 — 请检查 app_setting 表中 key='platform_miniprogram' 的记录"。用户说"之前订阅还可以"。
- 根因机制(`WxMiniprogramUtil.getAccessToken`):
- access_token 有 Redis 缓存,key=`WX_ACCESS_TOKEN:5`DEFAULT_TENANT_ID=5),TTL=7000秒(约2小时)。
- **缓存命中时直接返回 token,不读 app_setting 配置**;缓存未命中才调 `getMiniprogramSetting()``app_setting` 表 key='platform_miniprogram'。
- 所以"之前订阅可以"= access_token 缓存命中掩盖了配置读取问题;现在续费失败=缓存过期,走到读配置那步发现读不到。
- 这**不是续费特有 bug**,现在去测订阅(subscribe+pay)也会同样失败。
- `AppSettingServiceImpl.getByKey` 只按 setting_key 查、不按租户过滤(注释"平台配置不按租户隔离"),排除租户隔离问题。
- 排查方向(按可能性):
1. app_setting 表 platform_miniprogram 记录被误删/setting_value 被清空。
2. setting_value JSON 格式损坏(多/少逗号引号),`JSON.parseObject` 抛异常 → catch return null。
3. 表里有多条 setting_key='platform_miniprogram' 记录,MyBatis-Plus getOne 抛 TooManyResultsException → catch return null。
- 后端 handleWechatPay 开头会调 `diagnoseConfig()` 打印详细日志(app_setting 是否存在、setting_value 原始内容、解析出的 appId/appSecret),看后端日志最快定位。
- 修复:配置丢了去小程序配置页重新保存(batchSave 会写入);多条记录则清理只留一条。
- 关键文件:`/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/common/core/utils/WxMiniprogramUtil.java`getAccessToken line 182-276getMiniprogramSetting line 291-316)。
## 后端 renew-pay 隐患修复(放弃支付不中断服务 + 续费不丢时长)
- 隐患:原 renewPay 把原订阅 status 改 pending + 提前改 expireTime,用户放弃支付会服务中断、到期时间错乱;且 handleBalancePay/mpConfirm 激活时 expireTime=now+period 覆盖了续费应得的"原到期+period",未过期续费用户损失剩余时长。
- 后端改动 `AppSubscriptionController.java`4 处):
1. `renewPay`:删 `setStatus("pending")``setExpireTime(newExpireTime)`;只设 payStatus=0 + payPrice/originalPrice/period。原订阅 status 保持 active(或 expired),expireTime 不变 → 放弃支付零影响。newExpireTime 仍计算,仅用于日志"预计到期(支付后)"。
2. `handleBalancePay` 激活:`sub.setStartTime(now)``if (startTime==null) setStartTime(now)`(续费保留首次生效时间);expireTime 由 `now+period``max(原expireTime, now)+period`
3. `mpConfirm` 拦截:`if (status=="active")``if (status=="active" && payStatus==1)`,放行续费的 active+payStatus=0。
4. `mpConfirm` 激活:同 handleBalancePaystartTime 仅 null 时设、expireTime 用 max(原expireTime, now)+period。
- max 逻辑对新订阅和续费都正确:新订阅 expireTime 为 null → expireBase=now → now+period;续费未过期 → 原+period(不丢时长);续费已过期 → now+period。
- 放弃支付现状:订阅保持 active+payStatus=0,服务不中断;用户可再次点"立即续费"重新 renew-pay(会覆盖 payPrice/period 并重新生成码)。无清理 pending 续费订单的需求(不再产生 pending 记录)。
- 前端无需改动(renew-pay 调用不变)。
## 续费"配置不存在"真正根因:app_setting 未加入租户忽略列表(已修复)
- 纠正之前猜测:app_setting 表 platform_miniprogram 配置**完全正常**pymysql 直查 db_websopy 确认:setting_id=12, tenant_id=5, 169字节, JSON 合法, appId=wx541db955e7a62709, appSecret 齐全)。
- 真正根因:`MybatisPlusConfig.java``TenantLineHandler.ignoreTable` 列表里有 `app_subscription` 但**没有 `app_setting`**。
- app_setting 查询受租户拦截器隔离 → `getByKey('platform_miniprogram')` 被自动加 `AND tenant_id=当前租户`
- 配置记录 tenant_id=5;续费用户租户 ≠ 5(或请求头/域名取不到租户)→ 查询过滤掉这条记录 → 返回 null → 报"配置不存在或解析失败"。
- 订阅 pay 能用 = access_token 缓存命中(Redis key 固定 `WX_ACCESS_TOKEN:5` 不分租户,TTL 7000秒)没走 app_setting 查询;续费报错 = 缓存过期走了查询被拦截。与"之前还可以、现在不行"的时间差完全吻合。
- `AppSettingServiceImpl` 代码注释明说"平台配置不按租户隔离"、getByKey 不带租户条件,但拦截器在 SQL 层强制隔离,两者矛盾——这是历史遗留 bug。
- 修复(后端 `MybatisPlusConfig.java`):ignoreTable 列表在 `"app_subscription"` 后加 `"app_setting"`(一行改动)。改完 app_setting 所有查询忽略租户,与代码设计意图一致。
- 排查方法值得记录:pymysql 直连远程库(47.119.165.234:13308 db_websopy / redis 16379 db0)验证数据;mysql 客户端未装时用 managed python venv 装 pymysql 查。dev profile 连此库。
- 前端订阅 API 域名硬编码 `https://websopy-api.websoft.top/api/app/subscription`api/index.ts WEBSOPY_API_BASE),主站 API 走 guilixu-api.websoft.top。用户对比的 `websoft.top/api/_app/...` 是另一个入口(疑 nginx 转发 _app→app)。
## 续费"弹二维码自动完成支付"修复:清除旧 Redis 支付标记
- 现象:续费 renew-pay 弹出小程序码后几秒,前端轮询 checkStatus 返回 paid=true,自动判定支付成功(用户没扫码)。
- 根因(pymysql+redis 直查确认):续费复用原订阅记录的 subscriptionNo,原订阅支付时 mpConfirm/handleBalancePay 写入的 Redis 标记 `wxpay:paid:{subscriptionNo}=1`TTL 30分钟)未清除。checkStatus 逻辑 `paidViaRedis || paidViaDb`pay_status 维度(renewPay 设 0)已返回 false,但 Redis 维度残留 → 误判。
- 数据证据:id=94 status=active pay_status=0 expire_time=2027-06-15(订阅支付成功→renewPay 改 pay_status=0);Redis wxpay:paid:SUB202607152237396107=1 残留。
- 修复(方案A,用户确认):`AppSubscriptionController.renewPay` 在 updateById 后加 `redisUtil.delete("wxpay:paid:" + sub.getSubscriptionNo())`RedisUtil.delete 在 line 214)。配合 pay_status=0checkStatus 两个维度都 false → paid=false。
- 次要问题(未解决):续费把原订阅 pay_status 1→0,若放弃支付,原订阅变 active+pay_status=0(语义"已激活但未支付"),不影响功能。彻底解决需方案C(renewPay 新建独立订阅记录,原订阅不动),暂不做。
- 排查手段:managed python venv 装 pymysql+redis 直连远程库(47.119.165.234:13308/16379)查 app_subscription 表 + Redis scan wxpay:paid:*,比看日志更直接。
## cmsAd 编辑保存不再拼 OSS 压缩后缀
- 需求:广告编辑保存时图片 URL 不再拼接 `?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90` 后缀,因为图片压缩已统一在小程序端处理。
- 改动 `src/views/cms/cmsAd/components/cmsAdEdit.vue``chooseFile` 函数:
- `images.value.push``url``data.downloadUrl + '?x-oss-process=...'` 改为 `data.downloadUrl`
- `form.images` 同步去掉后缀,直接用 `data.downloadUrl`
- 注意:本次只改广告(cmsAd);`cmsArticle/articleEdit.vue``cmsModel/cmsModelEdit.vue` 同样有此后缀,用户未提及,暂不动。
+63
View File
@@ -0,0 +1,63 @@
# 2026-07-16
## 续费扫码支付链路修复(前后端联动)
### 问题1:小程序 pay 页扫码后误显"支付成功"
- 文件:`websopy-taro/src/passport/pay/index.tsx`
- 根因:`fetchDetail``subscription.status === 'active'` 判定已支付。但后端 `renewPay` 为了避免放弃支付导致服务中断,**故意保留** `status='active'`、仅设 `payStatus=0`
- 修复:判断改为 `payStatus === 1`;同时处理 `cancelled`/`expired` 状态。详见 `websopy-taro/.workbuddy/memory/2026-07-16.md`
### 问题2:用户点"立即支付"后端返回"该订阅已激活"
- 文件:`/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/app/controller/AppSubscriptionController.java`
- 根因:`pay/{id}`line 339-341)和 `mp-prepay`line 539-541)两处拦截只看 `status='active'`,与上次 `mp-confirm` 同样的坑——续费的 `active+payStatus=0` 被误拦。
- 修复:两处拦截条件对齐 `mp-confirm`,改为 `status='active' && payStatus==1` 才拦截,放行续费的 `active+payStatus=0`
- commit`7c95f8e` (本地 main,待 push + 服务器部署)。
### 关键规则(防回归)
判断"是否已支付"必须用 `payStatus`(0=未支付 / 1=已支付),**不能**用 `status`(订阅生命周期 active/pending/expired/cancelled)。续费场景下这两个字段完全解耦:后端 `renewPay` 保留 `status='active'` + 设 `payStatus=0`
后端三处拦截条件现在全部对齐:
- `pay/{id}` line 339-343
- `mp-prepay` line 541-545
- `mp-confirm` line 573(之前已修)
### 部署待办(用户操作)
1. 后端:`cd /Users/gxwebsoft/JAVA/websopy-java && git push`,然后远程服务器 `git pull && mvn clean package -DskipTests && docker-compose up -d --build cms-api`(或对应重启方式)。
2. 小程序端:`websopy-taro` 重新编译上传体验版/正式版。
3. 联调验证:admin 点"立即续费" → 弹码 → 微信扫码 → 进小程序应显示"确认支付"页(不再误显成功)→ 点"立即支付" → 微信支付 → 显示"支付成功" → 跳转 `/user/apps/index`
## 商城后台 Dashboard 布局调整
- 文件:`src/views/shop/dashboard/index.vue`
- 需求:今日数据概况页,「基本信息」改为左右两列并排;删除「快捷操作」面板。
- 改动:
- `a-descriptions``:column="1"` 改为 `:column="{ xs: 1, sm: 2 }"`(响应式两列并排)。
- 基本信息 `a-col``:span="12"` 改为 `:span="24"`(占满整行)。
- 删除「快捷操作」`a-col` 整块,并清理其专属的图标导入(ShoppingCartOutlined/ShopOutlined/AppstoreOutlined/GiftOutlined/TeamOutlined/SettingOutlined/ClearOutlined)、`removeSiteInfoCache` 导入及 `handleClearCache` 函数。
### 今日数据概况 + 基本信息改为左右两栏布局
- 需求:参考「待处理事项 + 快速入口」的左右结构,将「今日数据概况」和「基本信息」也并排为左右各半。
- 改动:
- 原来各自独占整行的两个 panel,合并到一个 `<a-row>` 中,各占 `:md="12"`
- 今日数据概况栅格从 4 列改为 2 列(`grid-template-columns: repeat(2, 1fr)`),适配半宽空间。
- 基本信息 `a-descriptions` 列数改为 `:column="1"`,半宽下单列更不拥挤。
- 基本信息(左)和今日数据概况(右)位置对调,基本信息在左、今日数据概况在右。
### 刷新统计数据修复
- 问题:点「刷新数据」按钮提示成功,但实际数据没更新。
- 根因:`refreshStatistics` 调用 `statisticsStore.fetchStatistics()` 未传 `forceRefresh=true`5分钟缓存有效期内直接返回旧数据。
- 修复:改为 `fetchStatistics(true)` 强制跳过缓存。
## README.md 修改
- 项目名称从"WebSoftAdmin"改为"小程序商城后台管理系统"。
- 项目域名更新为 https://shop.websoft.top。
- 去掉 CMS 内容管理系统的介绍部分(文章管理、一键排版、媒体库等)。
- 更新技术栈版本号对齐 package.jsonAntDv 4.x、Vite 8.x、TS 5.x、Pinia 2.x)。
- 补充门店/经销商/会员管理等商城核心功能描述,对齐实际 views/shop 目录结构。
+122
View File
@@ -0,0 +1,122 @@
# 2026-07-17 工作日志
## 订单发货功能改进(shopOrder / deliveryModal
分析了订单发货弹窗(`src/views/shop/shopOrder/components/deliveryModal.vue`)三种配送方式,按用户确认的改进方案实施了前端改动:
### 改动文件
1. `deliveryModal.vue`(核心)
- 新增「发货门店」下拉(复用 `listShopStore`),选中后自动回填:快递配送→发货人=门店名/电话=门店电话/地址=门店地址;商家送货→地址=门店地址。
- 商家送货(`deliveryType===2`)新增「配送店员」二级联动下拉(复用 `listShopStoreUser({storeId})`),选中店员自动回填发货人=店员名/联系方式=店员电话。
- 校验:发货门店必填(`deliveryType!==1`);配送店员必填(仅 `deliveryType===2`)。
- 提交时附带 `sendStoreId` / `deliveryUserId`
2. `api/shop/shopOrder/model/index.ts`:新增 `sendStoreId``deliveryUserId` 字段(为后端落库铺路)。
3. `orderInfo.vue`:发货信息卡片增加「发货门店」「配送店员」展示(可选链,后端返回 `storeName`/`deliveryUserName` 后自动生效,否则显示「未填写」)。
### 关键结论 / 待后端配合
- 门店、店员 list API 已存在,前端改动零后端依赖即可落地交互。
- **后端缺口(需补充)**`shop_order` 表需新增 `send_store_id``delivery_user_id` 字段存储门店/店员关联;否则下拉选了也存不进去。
- 登录用户 `User` 模型无 `storeId`,无法默认锁定当前门店,门店下拉需用户手动选择。
- 原「发货地址模板」需求因无现成后端接口,改用「门店作为发货地址模板」方案满足(复用门店 name/phone/address)。
## 配送方式排序调整
- 修改 `deliveryModal.vue` 配送方式 radio 按钮顺序:快递配送→商家送货→无需发货(原:快递配送→无需发货→商家送货),仅调换模板中 radio 排列顺序,value 值不变。
## 后端 Java 改动(guilixu-java,分两轮)
### 第一轮:shop_order 主表
- `ShopOrder.java` 实体加持久化字段 `sendStoreId``deliveryUserId`(→`send_store_id`/`delivery_user_id`),及虚拟展示字段 `sendStoreName`/`deliveryUserName`exist=false)。
- `ShopOrderMapper.xml``selectSql``LEFT JOIN shop_store f` / `LEFT JOIN shop_store_user g` 并 SELECT `f.name as sendStoreName, g.name as deliveryUserName`,让订单详情联带回门店/店员名称。
- 前端 `orderInfo.vue` 展示路径从 `form.shopOrderDelivery.xxx` 改为 `form.sendStoreName`/`form.deliveryUserName`(名称落在订单主对象上)。
- SQL`ALTER TABLE shop_order ADD send_store_id INT, delivery_user_id INT;`
### 第二轮:shop_order_delivery 发货单表(用户要求"也写进发货记录表")
- `ShopOrderDelivery.java` 实体加持久化字段 `sendStoreId``deliveryUserId`
- `ShopOrderController.update()` 的发货写入分支(无需物流分支 deliveryMethod=20、实物快递分支 deliveryMethod=30、以及补录运单号分支)新建 `ShopOrderDelivery` 时都 `setSendStoreId`/`setDeliveryUserId`(值取自 `shopOrder` 对象,兜底取 `shopOrderNow`)。
- 说明:前端发货只调 `PUT /shop/shop-order`,该接口内部 `new ShopOrderDelivery()` 并 save 落 `shop_order_delivery`;主表靠 `updateById(shopOrder)` 自动落库,发货单表需显式 set 字段。
- SQL`ALTER TABLE shop_order_delivery ADD send_store_id INT, delivery_user_id INT;`
- 验证:`vue-tsc` 前端零报错;Java 用 `./mvnw -o compile` 后台验证中。
## Taro 商家端 storeId=0 排查(guilixu-taro,跨项目)
- 用户反馈 orders 页请求 `/shop/shop-store-user?storeId=0`。排查结论:
- `guilixu-taro/src/pages/store/orders/index.tsx` **未调用** `shop-store-user`;全项目 `listShopStoreUser` 仅定义、无调用方。该请求非当前源码发出,应为线上旧版 H5 构建。
- **当前门店 storeId 的真实来源**:`getMyClerk()``GET /shop/shop-store-user/my`)返回的 `ShopStoreUser` 对象,其 `.storeId` 字段(shop_dealer.id)才是门店 id。注意 `user.tsx:103``storeInfo`=getMyClerk 结果)判断"是否显示门店中心",所以那里就能拿到 `storeId = storeInfo.storeId`
- **易错点**`ShopStoreUser``id`(店员自身主键)和 `storeId`(门店 id)两个字段,二者不同。误用 `storeInfo.id` 当门店 id 是 bug。`booking/index.tsx:186``storeId: storeInfo!.id || 0``storeInfo` 恰为 `ShopStore` 类型故 `.id` 碰巧正确,但 `|| 0` 兜底在未加载时仍会变 0。
- **根因模式**:项目无全局门店/店员上下文,`storeInfo` 是各页面局部 useStateuser/booking/center 各一份)。需在 storeId 就绪前发请求 + `|| 0` 兜底 → `storeId=0`
- **已实施全局 `useClerk`**(用户确认后开工):
- 新增 `src/contexts/ClerkContext.tsx``ClerkProvider` + `useClerkContext`。登录态(`useUserContext().isLoggedIn`)变化即拉取/清空 `getMyClerk()`;对外暴露 `clerk`(ShopStoreUser|null)、`storeId`(=clerk?.storeId)、`loading``refreshClerk``clearClerk`
- 新增 `src/hooks/useClerk.ts`:镜像 `useUser` 的"优先 Context、未包裹则降级本地"范式,文件头注释写明用法与 `storeId>0` 守卫约定。
- `src/app.tsx`:在 `UserProvider` 内挂载 `<ClerkProvider>`(使其能响应登录/登出)。
- 重构 `src/pages/user/user.tsx``src/pages/store/center/index.tsx`:移除各自的局部 `storeInfo`/`getMyClerk` 调用,改为 `const { clerk: storeInfo, refreshClerk } = useClerk()`
- 补全 `src/api/shop/shopStoreUser/model/index.ts``ShopStoreUser` 模型:`getMyClerk`(/my) 联表返回的 `name`/`phone`/`avatar`/`storeName` 补入(原模型缺失,旧代码靠 `useState<any>` 掩盖)。
- **验证**:裸 `tsc --noEmit --types webpack-env` 全量检查——新建/改动的 `ClerkContext.tsx`/`useClerk.ts`/`app.tsx`/`user.tsx`/`center/index.tsx` 及模型 **零新增报错**`center` 页仅剩 4 个**既有**报错(75 的 `finally`、82 的 `applyStatus`、89 的 `total`、106 的 `Option`,均在未改动的 `loadBadgeCounts`/`subscribe` 原函数里,属项目原有类型问题 / 裸 tsc 的 lib 配置 artifact)。注意裸 `tsc` 默认会因 tsconfig `types:["@tarojs/taro"]`+`typeRoots``TS2688`,需用 `--types webpack-env` 覆盖才能跑真实业务检查。
- **未动**`booking/index.tsx``storeInfo` 是用户所选门店(ShopStore,来自 `getShopStore(id)`/选择),与店员所属门店是两回事,不在本次范围(其 `storeId: storeInfo!.id || 0` 属不同接口的兜底问题)。orders 页现可随时从 `useClerk().storeId` 取当前门店 id。
## Taro 商家端 orders 页「选择发货人员」发货功能(guilixu-taro
- 用户截图显示线上旧版 orders 页有「选择发货人员」弹窗且请求 `?storeId=0` 显示"暂无可选择的店员"。当前源码 `orders/index.tsx` 原本**没有**该功能(只有确认收款/确认完成),确认是旧构建。
-`src/pages/store/orders/index.tsx` 新增「确认发货」功能:
- 引入 `useClerk()``storeId`(杜绝 storeId=0)、`listShopStoreUser``ShopStoreUser` 类型。
- `OpType` 增加 `'deliver'``getOrderActions` 中「待发货」订单(`payStatus && deliveryStatus===10`)显示"确认发货"按钮。
- `openModal('deliver')` 时调 `loadClerkList()``storeId>0``listShopStoreUser({storeId})`,否则 toast 提示门店未就绪、不发请求。
- 弹窗内新增店员单选列表(selectedClerk),deliver 模式隐藏凭证上传区。
- 提交发货:`deliveryType=2`(商家送货)、`deliveryStatus=20`(待收货)、`deliveryTime``sendName/sendPhone`(店员)、`sendStoreId=storeId``deliveryUserId=店员id``PUT /shop/shop-order`
- 后端已在上一轮把 `sendStoreId`/`deliveryUserId``shop_order` + `shop_order_delivery`,此处字段对齐。
- 验证:`tsc --noEmit --types webpack-env` 总报错仍 387(零新增);orders 页仅剩 2 个既有报错(165 `fetchLatestOrders` 返回类型、169 `count` 隐式 any,均在未改的 `useNewOrderDetector` 调用块)。
---
## ⚠️ 重大纠正:正确项目是 xinlong-shop-taro,不是 guilixu-taro
- 用户最终明确:商家端发货/门店相关功能在 **`/Users/gxwebsoft/VUE/xinlong-shop-taro`**,前面整段记的 `guilixu-taro` 改动**全改错项目**了。
- **已在 guilixu-taro 还原**git 仓库):`git checkout --` 还原 5 个跟踪文件(model/index.ts、app.tsx、center/index.tsx、orders/index.tsx、user.tsx);`rm` 删除 2 个新建文件(contexts/ClerkContext.tsx、hooks/useClerk.ts)。未动的 `pages/index/index.scss` 是仓库原有未提交改动,保留。
- **xinlong-shop-taro 才是真项目,且早已内置发货功能**:`orders/index.tsx` 已有 `OpType='ship'` 的「发货」+ `saveShopOrderDelivery` + `listShopStoreUser` + 发货弹窗;`user.tsx` 已有 `getMyClerk()``storeInfo`(含 `.storeId`)用于"门店中心"判断。
- **真正的 `storeId=0` bug 根因**`xinlong-shop-taro/src/pages/store/orders/index.tsx:465``openShipModal``listShopStoreUser()` **没传 storeId**(默认 0),即用户最初报的 `?storeId=0`
- **已修(xinlong-shop-taro**`openShipModal` 内先 `getMyClerk()``storeId``listShopStoreUser({storeId})` 拉本门店店员;并默认选中 `userId === 当前登录人 userId` 的店员(`setSelectedClerkId(match.id)`)。`tsc --noEmit --types webpack-env` 该文件零新增报错(仅 162/208/212 三个项目既有错误,均在我未改动区域)。
- **教训**:跨项目时先用 glob/find 在多个候选项目里确认"哪个项目有该功能源码",再动手;不要凭域名/习惯默认锁定一个项目。本次因 `guilixu-taro``xinlong-shop-taro` 命名相近、且前者恰有 `getMyClerk` 等同名 API 而误判。
## 发货弹窗文案优化(deliveryModal.vue
- 用户反馈"快递配送时发货人/联系人选门店是否不合理",分析结论:业务逻辑正确(发货人=寄件方=门店),问题在文案未区分"寄件方"与"承运方"。
- 改动 `src/views/shop/shopOrder/components/deliveryModal.vue`
- 「发货门店」标签动态化:快递配送→「发货门店」,商家送货→「配送门店」(placeholder 同步动态)。
- 「发货人」标签动态化:快递配送→「寄件人」,商家送货→「发货人」(联系方式标签同理)。
- 提示文案动态化:快递配送说明"寄件人即为发货门店",商家送货说明"回填配送店员信息"。
- 快递配送时在寄件人字段下方加小字说明"寄件人信息将同步至快递面单"。
- 验证规则的 reject 文案同步动态化。
- 不涉及数据结构/接口变更,纯前端文案优化。
## 修复小程序订单详情页配送方式判断逻辑(xinlong-shop-taro)⚠️ 注意正确项目
- **正确项目**`/Users/gxwebsoft/VUE/xinlong-shop-taro`(非 guilixu-taro
- 文件:`src/pages/order/detail.tsx`
- 问题:原代码 `isExpress = order.deliveryType === 0 || undefined`,无法区分 deliveryType=2(商家送货)
- 修复:
1. 新增 `DELIVERY_TYPE_MAP``0=快递配送, 1=自提, 2=商家送货`
2. 修正 `isExpress` 判断:`deliveryType === 0 || 2 || undefined` → 快递/商家送货都显示收货人信息
3. 收货信息卡片:商家送货显示🛵图标+配送门店名,快递配送显示📦图标
4. 订单信息区新增「配送方式」字段展示(使用 DELIVERY_TYPE_MAP
- deliveryType 值范围来自管理后台 deliveryModal.vue0=快递配送, 1=无需发货(自提), 2=商家送货
- ⚠️ 初次改错了项目(guilixu-taro),已用 git checkout 还原,正确改动在 xinlong-shop-taro
## 方案A:线下付款先发货后结款(三端同步改动)
### 后端(guilixu-java
1. `ShopOrderServiceImpl.confirmOfflinePayment()`:移除 `.eq(ShopOrder::getOrderStatus, 0)` 约束,允许已发货订单确认收款。新增逻辑:若 `deliveryStatus=20`(已发货),确认收款后自动 `setOrderStatus=1` 完成订单。
2. `ShopOrderController.confirmOfflinePayment()`:更新 `@Operation` 描述和成功消息(不再固定写"进入待发货状态")。
3. `ShopOrderService.java`:更新 Javadoc 描述。
4. **注意**Controller 的 `PUT /shop/shop-order`(update)接口发货时**不校验 payStatus**,仅判断 `deliveryStatus 10→20`,所以未付款的线下订单可以直接通过 update 接口发货(前端改 deliveryStatus 即可)。
### 管理后台(guilixu-admin
1. `src/views/shop/shopOrder/index.vue`:线下付款未付款订单(payType=9, payStatus=0, orderStatus=0)分两种情况:
- `deliveryStatus=10`:同时显示「确认收款」+「发货」+「关闭」
- `deliveryStatus=20`:仅显示「确认收款」(先发货后结款,已发货状态)
2. `src/views/shop/shopOrder/components/OfflinePaymentModal.vue`
- 新增 `deliveryStatus` 表单字段 + `alertMessage` 计算属性:已发货时提示"确认后订单将自动完成",未发货时提示"进入待发货状态"。
- 成功消息动态化:`deliveryStatus===20 ? '已自动完成' : '已进入待发货状态'`
3. `src/views/shop/shopOrder/components/orderInfo.vue`
- 支付状态标签:`payType=9 && payStatus=0 && deliveryStatus=20` → 蓝色「已发货待收款」
- `loadSteps()` 步骤条:线下付款未付款但已发货时,步骤条显示「下单→待收款→已发货」而非停留在「下单」
### 小程序端(xinlong-shop-taro
1. `src/pages/store/orders/index.tsx`
- `getStatusText()`:新增「已发货待收款」状态(`payType===9 && !payStatus && deliveryStatus===20`
- `getOrderActions()`:发货按钮条件放宽——线下付款未付款(`payType===9`)也能显示发货按钮(支持先发货后结款)
- `OP_DESC`:确认收款描述移除"订单将进入待发货状态"(已不是唯一路径)
+49
View File
@@ -0,0 +1,49 @@
# 2026-07-18 工作日志
## 手机号登录改用 loginBySuperAdminSms 接口
### 改动文件
1. `src/api/passport/login/index.ts`
- 新增 `loginBySuperAdminSms()` 方法,调用 `/loginBySuperAdminSms` 接口
- 保留原有 `loginBySms()` 方法(其他页面如 register/dealer 仍在使用)
- 新方法逻辑与 `loginBySms` 一致:成功后 setToken + 存 TenantId/UserId
2. `src/views/passport/login/index.vue`
- import 替换:`loginBySms``loginBySuperAdminSms`
- `onLoginBySms()` 函数中调用替换:`loginBySms(form)``loginBySuperAdminSms(form)`
- 多租户选择逻辑(`listAdminsByPhoneAll`)保持不变
### 注意
- 仅改了管理后台登录页,注册页面(register/dealer)仍用 `loginBySms`
## 账号登录合并邮箱/用户ID登录
### 需求
账号登录输入框同时支持「邮箱」和「用户ID」:邮箱 → `https://server.websoft.top/api/loginByEmail`,纯数字 → `https://server.websoft.top/api/loginByUserId`
### 改动文件
1. `src/api/passport/login/model/index.ts`
- `LoginParam` 新增 `email?: string``userId?: string | number`
2. `src/api/passport/login/index.ts`
- 新增 `loginByEmail()` / `loginByUserId()`,逻辑同 `login()`setToken + 存 TenantId/UserId
3. `src/views/passport/login/index.vue`
- 表单字段 `userId``account`(新增 `LoginForm = User & { account?: string }` 类型)
- 输入框 placeholder 改为「邮箱 / 用户ID」,form-item name 改为 `account`
- 校验规则 `userId``account`
- `submit()` 用正则判断邮箱(`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`) / 纯数字(`/^\d+$/`),路由到对应接口;格式不符提示「请输入正确的邮箱或用户ID」
- 移除未使用的 `login` 导入
### 注意
- 外部接口用绝对 URL`https://server.websoft.top/api/...`),未走 SERVER_API_URL 代理
- `vue-tsc` 全量检查仍有大量历史错误(ByteMdEditor、shopActivity 等),本次改动无新增错误
## 登录页右上角切换图标互换
### 需求
`src/views/passport/login/index.vue` 右上角扫码/手机切换按钮,图标原本与「目标模式」相反,需互换。
### 改动
- 第 65-66 行:`v-if="loginType === 'scan'"` 显示 `QrcodeOutlined` → 改为 `MobileOutlined``v-else` 显示 `MobileOutlined` → 改为 `QrcodeOutlined`
- 即:扫码模式下显示手机图标(切回账号/手机),账号/手机模式显示二维码图标(切到扫码)
+24
View File
@@ -0,0 +1,24 @@
# 2026-07-23
## 订单导出字段补齐(物流部发货用)
- 文件:`src/views/shop/shopOrder/components/search.vue``handleExport`
- 原来只导出 8 列(订单编号/订单标题[实际是 comments]/买家姓名/手机/实付金额/支付方式/付款时间/下单时间),且缺物流发货关键字段。
- 新增并补齐到 15 列:收货人、联系电话、收货地址、配送方式(0快递/1自提/2商家送货)、商品明细(商品名x数量)、商品总数量、实付金额、配送时间、自提码、发货/自提店铺、买家备注、支付方式、发货状态(10未发/20已发/30部分)、下单时间。
- 新增辅助函数:`getDeliveryTypeLabel` / `getDeliveryStatusLabel` / `getGoodsSummary` / `getShopName`
- 注意:枚举值与 `deliveryModal.vue` 一致(deliveryType 0快递 1无需发货/自提 2商家送货)。
- 环境提示:本机 Grep 工具因缺 libpcre2 不可用,搜代码改用 Python 脚本遍历。
## 导出跟随当前标签筛选(避免混入非待发货订单)
- index.vue:新增 `statusFilterForExport` computed = `getStatusFilterByTab(activeKey)`,并通过 `:status-filter` 传给 `<search>`
- search.vue:给 props 加 `statusFilter?`,导出请求改为 `listShopOrder({ ...where, statusFilter: props.statusFilter })`
- 效果:导出所见即所得——切到「待发货」标签再导出,只导出待发货;其它标签同理。全部标签下 statusFilter=undefined 不额外过滤。
## 修复:导出的商品明细为空
- 根因:`listShopOrder`(非分页列表接口)不返回 `orderGoods` 嵌套;页面表格/详情用的是 `pageShopOrder` 和单独的 `listShopOrderGoods` 取明细。
- 修复:导出时在 `search.vue` 中用 `Promise.all``orderId` 逐个调 `listShopOrderGoods({orderId})` 补齐商品明细,再写入「商品明细」列。
- 商品明细格式升级为 `商品名(规格) x数量`,规格取自 ShopOrderGoods.spec,便于物流发对型号。
- 单订单请求失败时 `.catch(() => [])` 兜底,不影响其它订单。
## 修复:商品总数量计算不对
- 根因:「商品总数量」列原本用订单级 `d.totalNum`,但 `listShopOrder` 列表接口未返回该字段(与 orderGoods 同理),导致算错/为空。
- 修复:新增 `getGoodsTotalNum(goods)`,对 `listShopOrderGoods` 取到的明细 `goodsList[idx]` 逐行 `totalNum` 求和。改动后「商品总数量」与「商品明细」列同源,必然对得上。
+81
View File
@@ -0,0 +1,81 @@
# 2026-07-24 工作日志
## shopGoods 商品列表排序机制分析
- 现状:列表「排序号」列(`src/views/shop/shopGoods/index.vue` 约 273-279 行)只做展示,**未启用 `sorter`**,列表默认也不按 `sortNumber` 排序(默认排序由后端决定,前端 `orders` 仅来自「创建时间」列的排序)。
- 编辑表单里「排序号」是 `a-input-number`0-9999,默认 100),且在 `rules` 里是 **必填**
- 已存在类似的布尔开关 `recommend`(列表内联切换,index.vue 约 47-58 行),「置顶」可复用同一 UI 模式。
- 结论倾向:纯排序号在商品多时难用,建议「置顶开关 + 少量置顶项内排序 + 其余按默认(创建时间/销量)」的混合方案,而非完全替换。
## 已实现:方案 B(商品置顶功能)
- 数据模型 `src/api/shop/shopGoods/model/index.ts`:新增 `isTop?: number`(置顶 1/不置顶 0)。
- 列表 `src/views/shop/shopGoods/index.vue`
- 新增「置顶」列,列表内联一键切换(复用 recommend 的 CheckOutlined/CloseOutlined 模式),新增 `onTop` 处理函数。
- 「排序号」列开启 `sorter: true`(之前只展示、不可排序)。
- 后端迁移脚本 `add-is-top-shop-goods.sql`:新增 `is_top` TINYINT 字段;分页默认排序需改为 `ORDER BY is_top DESC, sort_number ASC, create_time DESC`(后端不在本仓库,需手动改 page 接口 ORDER BY / 放开 orders 白名单)。
- 注意:置顶真正"排到最前"依赖后端默认排序改动;点击列排序也需后端放开 is_top / sort_number。
## 后端改动(/Users/gxwebsoft/JAVA/guilixu-javaSpring Boot + MyBatis-Plus
- `entity/ShopGoods.java`:新增 `private Integer isTop;`(映射列 is_top,沿用 camelCase→下划线策略;mapper 用 SELECT a.*,无需改 XML)。
- `service/impl/ShopGoodsServiceImpl.java`
- `pageRel``listRel` 默认排序由 `sort_number asc, create_time desc` 改为 `is_top desc, sort_number asc, create_time desc`(置顶优先,置顶内按排序号,其余按创建时间倒序)。
- `collectFromUrl` 采集默认值补 `goods.setIsTop(0)`(与 recommend 一致)。
- 排序字段无白名单:`PageParam` 直接按列名拼 ORDER BY,前端点「置顶」「排序号」列排序无需额外放开。
- DB 仍需执行 `add-is-top-shop-goods.sql`(在 admin 仓库根目录):`ALTER TABLE shop_goods ADD COLUMN is_top TINYINT(1) NOT NULL DEFAULT 0`
- 副作用提醒:默认排序改为置顶优先会影响所有调用 pageRel/listRel 的端(含小程序商品列表),置顶商品会浮到最前——通常正是期望行为。
## shopGoods 列表:商品编号移到名称下方
- `src/views/shop/shopGoods/index.vue`:删除独立的「编号」列(原 `code` 列),改为在「商品」列的名称下方显示「编号:{{ record.code }}」(灰色小字),节省一列横向空间。无 code 时不渲染该行。
## shopGoods 列表:三个价格合并为一列 + 与编辑弹窗对齐
- 原「市场价/价格/会员价」三列(字段 salePrice/price/dealerPrice)合并为单个「价格」列(`key: 'priceGroup'`width 150)。
- 单元格内堆叠显示,顺序与命名对齐编辑弹窗(`shopGoodsEdit.vue` 基本信息 Tab):价格(price,加粗头条)→ 市场价(salePrice)→ 会员价(dealerPrice);空值显示 `-`
- 列表展示用 priceGroup 自定义 `#bodyCell` 渲染;导出 Excel`handleExport`)仍保留三列独立价格,未改动。
## 商品分类(ShopGoodsCategory)多级分类调研
- 分类管理页 `/shop/shopGoodsCategory` 本身**支持**多级:模型有 parentId/children,编辑弹窗用 a-tree-select(toTreeData) 不限层级,列表 computeDepth 递归缩进。
- 但商品端选分类的级联组件 `src/components/SelectGoodsCategory/index.vue` 的 formatData/filterData 只映射 2~3 级;shopGoodsEdit 的 `chooseGoodsCategory` 写死 `value[1].value`/`value[0].label`(仅 2 级),3 级及以上会选错/选不中。
- 后端 `/shop/shop-goods-category` 是否限层级需查后端仓库确认(本仓库看不到)。
## 新增:忘记密码(找回密码)功能
- 参考项目:`/Users/gxwebsoft/VUE/website-admin/app/pages/login.vue`(已验证可用)。
- 新增页面 `src/views/passport/forget/index.vue`:复用登录页视觉风格,支持「邮箱找回 / 手机找回」两种 Tab,含验证码 60s 倒计时、密码强度校验(≥8 位且含字母+数字)、两次密码一致性校验。
- 路由:在 `src/router/routes.ts` 启用 `/forget`(登录页「忘记密码」链接 `push('/forget')` 原本就指向它,路由此前被注释)。
- 新增 API
- `src/api/passport/login/index.ts``sendEmailCaptcha(data,{skipTenant})` → POST /sendEmailCaptcha`sendSmsCaptchaByAdmin(data,{skipTenant})` → POST /sendSmsCaptchaByAdmin。
- `src/api/layout/index.ts``resetPassword(data)` → POST /resetPassword(带 X-Tenant-Skip:1);`src/api/layout/model/index.ts` 新增 `ResetPasswordParam`
- 关键改动:`src/utils/request.ts` 请求拦截器在请求头含 `X-Tenant-Skip` 时跳过注入 TenantId/CompanyId/MerchantId/Domain(登录前找回密码场景,避免串租户)。`vite build --outDir dist-verify` 验证通过(真实 `vite build` 因沙箱 safe-delete 守卫删 dist 拦截,非代码问题)。
- 后端同址 server.websoft.top/api,与 website-admin 共用一套找回密码接口。
## 优化:忘记密码页发送验证码前 3 次免图形验证
- 修改 `src/views/passport/forget/index.vue`:引入 `getCaptcha`,新增图形验证码弹窗(与 `register2/index.vue` 风格一致)。
-`localStorage``forget_send_count_${activeTab}_${account}` 记录每个邮箱/手机号的发送次数。
- 同一个账号前 3 次点击「获取验证码」直接调用 `sendEmailCaptcha`/`sendSmsCaptchaByAdmin`;第 4 次起弹出图形验证码弹窗,输入正确并点击「立即发送」后才真正发送。
- 失败时自动刷新图形验证码;弹窗关闭时重置 `codeLoading`
- 新增 i18n 键 `login.forgetPage.captchaTitle``captchaPlaceholder``sendNow`zh_CN/en)。
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-captcha` 编译通过。
## 优化:登录页手机号验证码前 3 次免图形验证
- 修改 `src/views/passport/login/index.vue`:手机号登录 Tab 下,「发送验证码」按钮由直接弹窗改为先计数判断。
-`localStorage``login_sms_send_count_${phone}` 记录每个手机号的发送次数。
- 同一个手机号前 3 次直接调用 `sendSmsCaptcha`;第 4 次起弹出原有图形验证码弹窗,输入正确并点击「立即发送」后才真正发送。
- 失败时自动刷新图形验证码;补充了 `.catch`/`.finally`,避免发送异常时按钮一直 loading。
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-login` 编译通过。
## 修复:邮箱登录图形验证码未校验
- **问题**`src/views/passport/login/index.vue` 第 651-555 行的验证码比对逻辑被注释掉,导致用户输入任意非空字符串即可通过前端校验。
- **修复**:取消注释 `form.code?.toLowerCase() !== text.value` 的校验块,恢复「输入错误则提示"验证码错误"并刷新验证码」的逻辑。
- **说明**:当前后端 `/captcha` 接口返回明文答案 `data.text`,前端直接比对即可。后续若后端改为返回 key 模式,需同步改为传 key 给后端校验。
## 新增:手机号登录多租户选择
- 参考项目:`/Users/gxwebsoft/VUE/website-admin/app/pages/login.vue`
- `src/api/passport/login/model/index.ts`
- `LoginResult` 新增 `tenants?: TenantOption[]`
- 新增 `TenantOption` 类型与 `LoginOutcome` 联合类型(`ok` / `selectTenant`)。
- `src/api/passport/login/index.ts`
- 新增 `saveLoginState` 统一保存 token、TenantId、UserId
- `loginBySuperAdminSms` 改为返回 `LoginOutcome`,检测 `data.tenants` 时返回 `selectTenant`
- 新增 `loginBySelectTenant({ phone, code, tenantId, remember })` 二次登录接口。
- `src/views/passport/login/index.vue`
- `onLoginBySms` 根据 `outcome.status` 处理:单租户直接登录成功;多租户弹出「请选择要登录的租户」弹窗。
- 新增弹窗与 `onSelectTenant`:选择租户后调用 `loginBySelectTenant` 完成二次登录并跳转。
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-tenant` 编译通过。
+16
View File
@@ -0,0 +1,16 @@
# 2026-07-25 工作日志
## CMS 模块复用方案分析(mp-java vs guilixu-java
- 结论:**guilixu 应直接调用 mp-java 的 cms(modules 服务),不要整体复制到 guilixu-java。**
- 证据:guilixu 的 152 个 cms 类是 mp-java 235 类的子集分叉且落后 1.5 个月;前端 cms 接口已全部走 `MODULES_API_URL`(指向 mp-java),guilixu-java 的 cms 包是死代码;"整体复制"因缺 `project` 模块无法编译。
- 产出报告:`docs/cms-module-reuse-analysis.md`
- 已同步架构结论到 MEMORY.mdJava 后端架构 / MODULES_API_URL 模式)
## 商品分类列表:二级/三级前加树形连接线(方案A)
- 文件:`src/views/shop/shopGoodsCategory/index.vue`
- 需求:用户要二级/三级分类前加图标区分层级,先看 mockup 选方案,选了「方案A 树形连接线」。
- 实现:移除原 `a-space``paddingLeft` 纯缩进,改为用 SVG 绘制树形连接线(`renderTreePrefix` 函数)。
- 新增 `nodeMap``isLastMap``buildTreeMeta` 在 datasource 加载时构建(是否末位子节点)。
- 连接线逻辑:祖先节点非末位→贯穿引导竖线;末段→竖线+横线拐角(末位用 └,否则 ├)。每级宽 20px,颜色 #d0d0d0
- 顶级(depth=0)不画;二级/三级按祖先链回溯绘制。缩进完全由 SVG 宽度承担。
- 验证:vite 模块编译通过(localhost:5580 dev server 已起)。
+24
View File
@@ -0,0 +1,24 @@
# 2026-07-28
## 订单列表操作列分隔线对齐 shopGoods
- 文件:`src/views/shop/shopOrder/index.vue``column.key === 'action'` 操作列模板。
- 改动:原本操作列按钮只用 `<a-space>` 包裹、无分隔线;现按 `shop/shopGoods` 的写法,在「详情」与各条件块之间、以及块内多个按钮之间加入 `<a-divider type="vertical" />`
- 分隔线放在各 `<template v-if>` 条件块内部,仅当对应操作出现时才渲染,避免多余竖线。
## 订单列表商品明细展开/收起(方案一已落地)
- 文件:`src/views/shop/shopOrder/index.vue`
- 需求:商品明细较多时列表行被撑高,加「展开/收起」。采用方案一(单元格内展开),已确认执行。
- 实现:
- 新增 `expandedOrderIds: ref<Set<string>>`(用 `String(record.orderId)` 作 key,因 orderId 为 number|undefined)。
- 新增 `collapsedGoodsCount = 2``isOrderGoodsExpanded(record)``toggleOrderGoodsExpanded(record)`
- 改写 `column.key === 'orderGoods'` 模板:默认 `record.orderGoods.slice(0, 2)`,超过 2 件显示「展开全部 N 件」,点击切换,展开后文案变「收起」。
- 类型检查:`vue-tsc --noEmit` 仅有的 3 处报错(line 2 `$router`/page-header、line 696/734 未用变量)均为项目既有,与本次改动无关。
## shop/dashboard 运行天数改为取租户创建时间
- 文件:`src/views/shop/dashboard/index.vue`
- 需求:/dashboard 的"运行天数"要改用「租户创建时间」而不是订阅生效时间。
- 现状调查:
- `cms/dashboard`(站点控制台,标题"系统运行天数")已在提交 894592c 改为 `getTenantInfo().createTime`(租户创建时间),已是目标行为。
- `shop/dashboard`(商城后台,标签"运行天数")原用 `currentSubscription.startTime`(订阅生效时间)。
- 改动:将 shop/dashboard 的 `runDays` computed 改为取 `tenantStore.company?.createTime`loadData 已 `tenantStore.fetchTenantInfo()` 写入 company)。同步更新顶部数据来源注释。
- 数据源说明:`getTenantInfo()` 返回 `Company`,其 `createTime` 字段即租户创建时间(`src/api/system/company/model/index.ts` line 164);`site.ts``runDays` getter 仍用 `siteInfo.createTime`(站点创建时间),目前 cms/dashboard 已不依赖它。
+16
View File
@@ -0,0 +1,16 @@
# 2026-07-29
## websopy-javacms 模块数据源路由修复(重要跨项目)
- 报错:`cms-template/current``Table 'db_websopy.cms_website' doesn't exist`jar 为 `websopy-api-1.5.0.jar`= `/Users/gxwebsoft/JAVA/websopy-java`)。
- 根因:websopy-java 原只有单一数据源,dev/prod/websopy 三个 profile 全指向 `db_websopy`;而 cms 模块(23 张 cms_* 表)实际在 `modules` 库。test profile 本就指向 `modules`(印证 cms 表在 modules)。
- 已确认方案 B(用户选「独立库 db_websopy」):多数据源,websopy 自有数据留 `db_websopy`,仅 cms 包走 `modules`
- 改动(在 websopy-java):
1. `pom.xml` 增加 `com.baomidou:dynamic-datasource-spring-boot-starter:3.4.1`(匹配 MP 3.4.3.3**不要用 3.5.x**,其对 MP 3.5,运行期可能 NoSuchMethod)。
2. `application-dev/prod/websopy/test.yml` 的单一 `spring.datasource` 改为 `spring.datasource.dynamic``primary: websopy`test 为 `modules`),`datasource.websopy` + `datasource.modules` 双库;modules 用库 `modules`、用户 `modules`、密码 `P7KsAyDXG8YdLnkA`(取自 test profile)。
3.`com/gxwebsoft/cms/mapper` 下全部 23 个 Mapper 接口加 `@DS("modules")`import `com.baomidou.dynamic.datasource.annotation.DS`),仅把 cms SQL 路由到 modules,不影响 websopy 自有表。
- 调用链:`cms-template/current``CmsWebsiteServiceImpl.getByTenantId()``baseMapper.getByTenantId()`(已注解)→ modules。✅
- 重新构建:websopy-java 本地无 mvn/docker,需在其环境 `mvn clean package` 或重 build docker 镜像后重启容器(prod profile `SPRING_PROFILES_ACTIVE=prod`)。
- ⚠️ 遗留问题(未处理,需确认):
- `shop/mapper/xml/ShopGoodsMapper.xml` 第 9 行 `LEFT JOIN cms_navigation`(无 schema 前缀),该 shop 查询走默认 websopy → 若 `cms_navigation` 不在 `db_websopy` 会失败;dynamic-datasource 无法跨单条 SQL 双库,需把 cms_navigation 复制到 db_websopy 或改成 `modules.cms_navigation`/`gxwebsoft_core.cms_navigation` 前缀。
- 前提假设:所有 cms_* 表都在 modules(依 test profile 整库连 modules 推断)。若实际只有部分 cms 表在 modules,应改为只给缺失的那几张 mapper 加 @DS
- 跨库写事务:cms 写操作经 @DS 走 modules 独立连接,与 websopy 事务不在一个事务里,写一致性需后续评估。
+27
View File
@@ -0,0 +1,27 @@
# 2026-08-01 工作记录
## 订单自动取消改造:超时时间改为后台「订单设置」驱动
### 需求
自动取消未付款订单的超时时间,需由管理后台「商城设置 → 订单设置 → 未付款自动关闭」控制;设置为 0 分钟 = 不自动取消。
### 改造要点(已全部落地)
- 后台设置存于 `ShopSetting``setting_key='shop_order'` 的 JSON 中,字段 `orderAutoCloseMins`(前端 order.vue 已支持保存)。
- 新增 `ShopSettingService.getOrderAutoCloseMinutes()`:参照 `getSalesAccumulateType()` 写法,从 `getCategoryValues("order")``orderAutoCloseMins`;0=禁用(合法返回);负数/读取失败兜底 30(常量 `DEFAULT_AUTO_CLOSE_MINUTES=30`)。
- `OrderAutoCancelTask.cancelExpiredOrders()`:先读 DB`==0` 直接 return(全局禁用,含租户特殊配置 tenantConfigs 一起跳过);`>0` 用它替换原先 YAML 的 `defaultTimeoutMinutes` 传给 `findExpiredUnpaidOrders`
- `OrderConfigProperties.AutoCancel.defaultTimeoutMinutes``@Deprecated`,仅作兜底。
- 前端 `order.vue``orderAutoCloseMins``min` 由 1 改 0placeholder/文案/校验提示改为「0 表示不自动取消」。
### 设计决策(用户已确认)
1. DB 未配置/异常兜底 = 30 分钟(与历史默认一致)
2. 现状 YAML `tenantConfigs`(租户特殊超时)保留不做改造;0=全局禁用时 tenantConfigs 也不执行
3. 不引入缓存,定时任务每次查一次 DB(开销极小),改后台 1~5 分钟内生效
4. 前端交互全部调整(min=0 + 文案 + 校验)
### 涉及文件
- JAVA: `ShopSettingService.java``ShopSettingServiceImpl.java``task/OrderAutoCancelTask.java``config/OrderConfigProperties.java`
- 前端: `src/views/shop/shopSetting/components/order.vue`
### 未做
- 未编译验证(本地无 maven/mvnw 不可用);改动为标准增量,已逐文件核对语法。
- SQL / 取消逻辑 / 库存回退 / 优惠券退还 / 销量回滚 全部未动。
+90
View File
@@ -0,0 +1,90 @@
# 2026-08-05 商品管理页(shopGoods)加载优化分析
## 关键结论(修正 2026-08-04 误判)
- 后端 `mp-java``ShopGoodsMapper.xml` 已支持 `sceneType` 参数:
- `on_sale``AND a.status = 0`
- `pending``AND a.status != 0`
- `sold_out``AND a.stock = 0`
- 前端 `search.vue``where.sceneType``pageShopGoods` 是**有效的**(JS 运行时携带,TS 类型未声明但不影响运行)。
- 结论:「出售中/待上架/已售罄」状态筛选功能**正常**,**不是卡顿根因**。上轮判定"sceneType 后端不识别"为误判。
## getCount 合并进 pageShopGoods 的影响面
- 商品模块 `getCount``/shop/shop-goods/data`)仅被 `src/views/shop/shopGoods/components/search.vue` **一处**调用。
- `cms/cmsArticle``getCount` 是独立接口(`/cms/cms-article/data`),互不影响。
- `guilixu-taro`(用户端小程序)未调用 `/shop/shop-goods/data`
- 合并到 page 响应、保留 `/data` 接口 → 向后兼容,零影响其他端。
- 收益有限:当前 `getCount``page``reload()` 里已是并发触发,合并仅省一次 HTTP 往返,非量级差异。
## 首屏请求并发情况
- 首屏同一帧发 3 个请求:pageShopGoodsdatasource)、getCountsearch.vue reload)、listShopGoodsCategoryindex.vue 顶层 query())。
- `search.vue``watch(() => props.merchantId, ..., { immediate: true })``index.vue` 顶层 `query()` 存在时序竞争,但单次进入只触发一次,未构成死循环。
## 卡顿真因(待 profiling 确认,未执行改动)
- 头号嫌疑:商品封面 `<a-image>`Ant Design Vue)不支持原生 `loading="lazy"`,首屏多行图片一次性并发请求;另 `getCompressedImageUrl` 每次 v-for 渲染重算。
- 次因:首屏 3 并发 + watch immediate 竞争。
- 建议:让用户开 DevTools → Network 看 `pageShopGoods` 耗时与是否有重复/慢请求,再定改动优先级。
## 用户决策(本轮)
- 已售罄=库存为0(与后端一致,确认)。
- getCount 合并影响面小但收益有限,待用户定。
- 导出(listShopGoods 全量)本轮先不做。
- 澄清:要替换的是商品封面 `<a-image>`**不是 ele-pro-table 整体**;仅当前模块。
## 后端 /datagetCount)实现细节(影响合并决策)
- `ShopGoodsController.data()` 实际执行:
1. `count(status=0 AND stock>0)` → totalNum(出售中)
2. `count(status>0)` → totalNum2(待上架)
3. `count(stock=0)` → totalNum3(已售罄)
4. **`update shop_goods SET status=1 WHERE stock=0`** ← 写副作用!每次调 /data 都会把已售罄商品自动下架。
- `/page` 用 MyBatis-Plus `selectPageRel`,内部已含 1 次 count(total) + 1 次 select,共 2 SQL。
- 现状进入/筛选:page(2 SQL) + data(3 count + 1 update = 4 SQL) 并发。
- 翻页/排序:只调 page(2 SQL)**不触发 data 的 update**。
## getCount 合并进 /page 的后端消耗结论(负优化,不建议)
- 合并后每次 page 调用(含翻页/排序)都多跑 3 次 count + 1 次 update → 翻页/排序 SQL 从 2 飙到 6。
- 列表返回变慢:原来 page 的 2 SQL 快速回,data 的 4 SQL 异步在后;合并后列表要等额外 3 count + 1 update 算完才返回。
- 写副作用被高频触发:翻页/排序也跑 `UPDATE ... WHERE stock=0`,幂等但带来行锁竞争,且写操作混在读链路是反模式。
- 总 DB 读次数在「进入/筛选」场景不变,但「翻页/排序」场景陡增;净收益为负(仅省一次 HTTP 往返)。
- **结论:单纯合并是负优化,不执行。** 真要优化后端统计应:① 3 次 count 合成 1 次 GROUP BY 条件聚合;② 把 update 副作用从读接口剥离(移到定时任务/下单扣库存时);③ 再谈是否挂到 page 响应。前端侧保持并发(Promise.all)即可,零后端改动。
## 已执行的前端优化(2026-08-05
用户确认执行,已改 3 个文件:
### 1. 商品封面图懒加载(index.vue + image.ts)★ 最可能见效
- `src/views/shop/shopGoods/index.vue` 第 39 行附近:`<a-image :src :preview=false :width :height>` 替换为原生
`<img class="goods-thumb" loading="lazy" width="50" height="50" alt="">`
- 原因:antdv `a-image` 不支持转发原生 `loading="lazy"`,且即使 `:preview=false` 仍包一层容器/事件;换成原生 `<img>` 后由浏览器 IntersectionObserver 懒加载,首屏仅可视区~10 张图请求。
- 新增 `.goods-thumb` 样式(object-fit:cover、圆角 4px、暗色占位背景)。
- `src/utils/image.ts``getCompressedImageUrl``Map<string,string>` 缓存,避免列表重渲染(勾选/hover/滚动)时重复拼接 OSS URL 字符串。全局受益,纯函数无副作用。
### 2. 首屏请求收敛(index.vue + search.vue
- `index.vue``<ele-pro-table>``:init-load="false"`(关闭表格自身自动首屏加载)。
- `search.vue`:去掉 `watch(merchantId, {...immediate:true})`,改为 `onMounted` 主动触发一次 `reload()`(设置 where.merchantId + 发 getCount + emit search 触发表格首次加载)。
- 原因:原 `initLoad`(默认 true) + `watch immediate` 导致首屏重复发一次 `pageShopGoods`(自动加载一次 + watch 又 reload 一次);改后首屏只有 1 次 page + 1 次 getCount(并发),无重复。
- `index.vue``query()`(加载分类)从顶层同步调用改为 `onMounted(query)`,分类数据延迟到列表首屏渲染后异步加载,不阻塞首屏;去掉 `query()` 内无用的 `loading.value = true` 写入。
- 注:`loading` ref 仍保留(handleExport 用它做防重入,非死变量,上轮"loading 死变量"判断有误)。
### 验证
- 已跑 `vite build` 后台验证可编译(待结果)。
## 后端优化已执行(2026-08-05,用户确认"现在改"
改 mp-java 共 6 处:
1. `ShopGoodsMapper.xml`:新增 `selectCountSummary` —— `CASE WHEN` 聚合一条 SQL 返回 totalNum(出售中=status=0且stock>0)/totalNum2(待上架=status>0)/totalNum3(已售罄=stock=0),替代原 3 次 count;含 `param.merchantId` 可选条件,多租户插件自动加 tenant_id。
2. `ShopGoodsMapper.java`:加 `selectCountSummary` + `downSoldOutGoods``@InterceptorIgnore(tenantLine=true)` 跨租户 `UPDATE shop_goods SET status=1 WHERE stock=0 AND status!=1`,加 `status!=1` 避免重复写)。
3. `ShopGoodsService.java`/`Impl``getCountSummary(param)`(聚合统计,Map→int 安全转换)+ `downSoldOutGoods()`
4. `ShopGoodsController.data()`:改为调 `getCountSummary(param)`,删除 3 次 count 与 update 写操作(data 接口不再写库)。
5. 新建 `shop/task/GoodsSoldOutTask.java``@Component` + `@Scheduled(cron="${shop.goods.sold-out.cron:0 */5 * * * ?}")`,每 5 分钟全局下架已售罄商品(仿 CouponExpireTask 写法,@EnableScheduling 已在 WebSoftApplication 启用)。
- 编译验证:IntelliJ 内置 Maven `compile` 通过(target/classes 已生成 GoodsSoldOutTask/ServiceImpl/Mapper.class)。
- **语义变化(需用户知晓)**
- 下架时机:原"每次打开商品列表才下架当前租户" → 现"定时任务每 5 分钟全局下架所有租户"。5 分钟窗口内新售罄商品会短暂留在"出售中"列表(on_sale 只查 status=0),直至定时任务执行。
- 范围:原依赖请求线程租户上下文(只下当前租户)→ 现 `@InterceptorIgnore` 跨租户(所有商户已售罄都下架),更彻底一致。
- 频率可调:`shop.goods.sold-out.cron` 配置项。
- 若要求"出售中"列表立即可不显示 stock=0,可给 on_sale SQL 加 `AND a.stock > 0`(未做,超出剥离范围,待用户决定)。
- 注:Controller 改造后 `LambdaQueryWrapper`/`LambdaUpdateWrapper` import 可能 unused,但 Java 编译允许 unused import 且其他接口可能仍用,未清理(编译已通过)。
## ⚠️ 重要修正:guilixu-java 才是本次后端优化的目标端(2026-08-05 用户指出"改错路径"
- 用户明确:后端改动**应该**落在 `/Users/gxwebsoft/JAVA/guilixu-java`,且 **mp-java 也保留**(两端都要有)。
- 原因澄清:shopGoods 的 `ShopGoodsController.data()` / Service / Mapper / task 在 **mp-java 与 guilixu-java 都存在且结构一致(同 `com.gxwebsoft.shop` 包)**,所以"共享 shop 业务模块"的改动要**两端同步**。
- 已对 guilixu-java 等价移植上述 6 处改动(selectCountSummary SQL、selectCountSummary/downSoldOutGoods 方法、getCountSummary/downSoldOutGoods 方法体、data() 剥离写、新建 GoodsSoldOutTask),与 mp-java 完全一致。
- 验证:IntelliJ 内置 Maven `compile` 通过(target/classes 已生成 GoodsSoldOutTask/ServiceImpl/Mapper/Controller.class)。
- 教训:之前 MEMORY.md 说"guilixu-java 的 cms 是死代码、前端不调 guilixu-java"是**针对 cms 模块**的;**不能推广到所有 shop 模块**——shopGoods 等共享业务在两端都有活代码,改动需两端同步。
+19
View File
@@ -0,0 +1,19 @@
# 2026-08-06 工作日志
## 运费模板「不发货地区」需求分析
- 客户需求:运费模板要支持"排除地区/不发货地区"(黑名单模式),而非仅"指定配送"(白名单)。
- 现状问题:当前 `ShopExpressTemplateDetail``provinceId`+`cityId` 单值,级联选择器只能选 1 个省市,无排除逻辑。截图里的双栏 Transfer modal 前端代码库里不存在,需从零开发。
- 产出接口方案文档:`docs/运费模板-不发货地区-接口方案.md`,含表字段变更(region_mode/region_ids)、下单校验逻辑、6 个待后端确认问题。
- 后端在 mp-java 的 shop 模块(前端走 MODULES_API_URL)。用户下一步:先与后端对齐接口方案,再做前端改造。
- 地区 ID 为行政区划编码字符串(省110000/市110100/区110101),下单时可前缀匹配。
- 存储方案确认:region_ids 采用**逗号分隔单字段**VARCHAR(2000)),不新建关联表。理由:查询方向固定为"模板→明细"、排除地区少、改动最小可快速上线。已写入方案文档。
- 用户确认"分两步"策略:第1步接口方案已产出(docs/运费模板-不发货地区-接口方案.md),待后端回复 6 个确认问题后做第2步前端改造。
## 后端实现检查(用户要求检查 guilixu-java 是否实现完)
- **guilixu-java 已实现完整**entity 加 regionMode/regionIds、param 加查询字段、迁移 SQL(ALTER+历史数据迁移+索引+回滚)、DeliveryRegionChecker(黑名单命中抛"暂不配送")、RegionCodeResolver(名称→编码)、OrderBusinessService 第 662/668 行已接入校验。
- **用户纠正(18:09):** guilixu-admin 的**商城接口实际调 `https://guilixu-api.websoft.top`= guilixu-java**,不是 mp-java。前端 shopExpressTemplateDetail API 用默认 `@/utils/request`baseURL = `MODULES_API_URL` = `localStorage ApiUrl || VITE_API_URL`;该部署的 ApiUrl 配的就是 guilixu-api.websoft.top/api,即 **guilixu-java 才是生效后端**
- **结论反转:guilixu-java 这套实现正确且生效,不是死代码,后端已完整,无需改 mp-java。** mp-java 与此功能无关(本部署 商城不走 mp-java)。
- 后端 `region/regions-data.json` 用 v/l/c 精简格式(与 RegionCodeResolver 读取一致),无字段名 bug。
- 后端完成度:entity/param/SQL/DeliveryRegionChecker/RegionCodeResolver/下单接入(OrderBusinessService 第 87 行调用 validateDeliveryRegionIfNeeded)全齐。
- **剩余工作 = 纯前端(guilixu-admin**:① model 加 regionMode/regionIds;② 新建双栏多选地区组件(替换单路径级联 `RegionsSelect`,支持"全国/指定配送/指定不发货"模式切换+多选多省);③ 编辑表单加模式切换+多选;④ 列表回显模式与地区汇总。截图里的 Transfer Modal 前端尚不存在。
- 教训:判断"改了哪个后端才生效"要以**运行时的 ApiUrllocalStorage/localStorage 配置的 ApiUrl,本部署=guilixu-java**为准,不能默认 `MODULES_API_URL`=mp-java。guilixu-admin 的 商城走 guilixu-java。
+130
View File
@@ -0,0 +1,130 @@
# 2026-08-11 工作记录
## 运费模板配送地区改为「省+市两级多选穿梭弹窗」
### 需求背景
客户要求运费模板的配送地区选择改成仿截图的双栏穿梭弹窗(左侧候选 + 右侧已选),
选择粒度从原「省+市单选(级联)」升级为「多条规则覆盖多城市」,并保留「不选=全国」规则。
### 改动清单(纯前端,后端字段由用户确认可改)
1. **新建组件** `src/components/RegionMultiSelect/index.vue`
- 树形可展开:省级可展开看市级;省级"选择"=选该省全部市,市级可单独选。
- 左栏搜索(按省/市名过滤)、右栏已选列表(单项是"省/市"+取消)。
- 底部:全部选择 / 取消 / 确认。
- `v-model:value` 绑定已选**城市ID数组**;额外 emit `update:provinceIds`(由城市推导的省份ID数组)。
- 复用 `src/components/RegionsSelect/load-data.ts` 的 getRegionsData()public/json/regions-data.json,取省+市两级,丢弃区级)。
2. **model** `src/api/shop/shopExpressTemplateDetail/model/index.ts`
- 新增 `provinceIds?: string`(逗号分隔)、`cityIds?: string`(逗号分隔);保留旧 `provinceId/cityId` 兼容历史数据。
3. **编辑弹窗** `src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue`
- 移除 `RegionsSelect type="provinceCity"`,改用 `RegionMultiSelect`
- 打开时从 `record.cityIds`(兼容旧 `cityId`)回填;保存时数组→逗号串提交 `cityIds/provinceIds`
4. **列表页** `src/views/shop/shopExpressTemplateDetail/index.vue`
- "配送地区"列 customRender 改为遍历 `cityIds` 多值反查 regionMap;超22字截断+tooltip;旧单值数据兼容。
- 新增 `h` 函数 import。
### 待办 / 风险
- antd 为 v4`var(--ant-color-border)` 在暗色下可能回退浅灰(边框略浅,不影响功能);如要完美暗色适配可后续改 less 变量。
- 构建验证(vite build)已通过。
## ⚠️ 重大修订(同日):前端字段名与后端不匹配,导致"没存进库"
### 根因
- 后端 `ShopExpressTemplateDetail` 实体**已有**配送区域多选机制:`regionMode`(0=全国 / 1=指定配送白名单 / 2=不发货黑名单)+ `regionIds`(逗号分隔的 6 位城市编码串)。
- 旧代码 `provinceId`/`cityId` 是单值历史字段,已被迁移脚本 `sql/shop_express_template_detail_region.sql` 迁成 `regionMode=1, regionIds=城市编码`
- 我第一版前端提交的是 `provinceIds`/`cityIds`,后端实体无此字段 → MyBatis-Plus 自动忽略未知字段 → 看起来"没存没变化"。
- ORM 是 MyBatis-Plusadd/update 用 `save()`/`updateById()` 自动生成 SQL,接收的是实体类本身(非 Param);新增字段只需在**实体加属性 + 表加列**,无需改 Mapper XML。
### 修复(对接后端已有的 regionMode/regionIds,无需后端改动)
1. model`provinceIds`/`cityIds` → 改为 `regionMode?``regionIds?`
2. `RegionMultiSelect` 组件:去掉 `update:provinceIds` emit,只 emit `update:value`(城市编码数组)。
3. 编辑弹窗:城市选择变化 → `form.regionIds = 逗号串``form.regionMode = 1`(有选)或 `0`(全国);回显从 `form.regionIds` 拆出。
4. 列表页:customRender 读 `record.regionIds` 反查城市名。
- 结论:**本需求纯前端即可闭环,后端无需改**。城市编码(`regions-data.json` 的 city value6位)与 `regionIds` 完全一致,下单校验 `DeliveryRegionChecker``regionIds` 直接生效。
## 修复:多规格SKU表格缺少库存列
- 文件:`src/views/shop/shopGoods/components/shopGoodsEdit.vue`
- 问题:`columns` 数组缺少 `stock`(库存) 和 `salePrice`(划线价) 列定义,导致多规格SKU明细表不显示库存输入框(批量设置栏有,但表格列没有)
- 修复:在 columns 数组中补齐 `salePrice``stock` 两列(bodyCell 模板已有渲染逻辑,只需加列定义)
## 修复:商品保存「快递配送时运费模板必选」前端校验缺失
- 文件:`src/views/shop/shopGoods/components/shopGoodsEdit.vue`
- 问题:后端有校验规则——`canExpress=1` 时要求 `expressTemplateId` 必填,但前端无对应校验,用户开启快递配送后直接提交被后端拦住,报错"运费模板必选"。
- 修复:在 `save()` 的 validate 通过后、请求前增加前端拦截:`canExpress=1 && !expressTemplateId``message.error('请选择运费模板')`
- UI 优化:运费模板 `<a-form-item>``:required="form.canExpress === 1"` 动态必填标记 + placeholder 提示文字。
## 修复:多规格SKU会员价(buyingPrice)保存失败
- **根因**:后端 `ShopGoodsSku` 实体类(guilixu-java + mp-java)和数据库表 `shop_goods_sku` 均无 `buying_price` 字段。前端提交的 buyingPrice 被 MyBatis-Plus 忽略 → 保存后丢失。
- **改动(三端同步)**
1. **guilixu-java** `entity/ShopGoodsSku.java`:加 `private BigDecimal buyingPrice;`
2. **mp-java** `entity/ShopGoodsSku.java`:同上(shop 模块两端同步)
3. **前端 TS 接口** `api/shop/shopGoodsSku/model/index.ts`:加 `buyingPrice?: string;`
- **需手动执行 SQL**`ALTER TABLE shop_goods_sku ADD COLUMN buying_price DECIMAL(10,2) DEFAULT NULL COMMENT '会员价' AFTER cost;`
## 修复:多规格SKU图片删不掉
- 文件:`src/views/shop/shopGoods/components/shopGoodsEdit.vue`
- 问题:`onDeleteSkuItem(index)` 误删 `images.value`(主商品图片数组),而非 `skuList.value[index].images`(对应SKU行图片)→ 点击 × 无反应
- 修复:改为清空 `skuList.value[index].images = []` + `image = ''`
## 商品列表「价格」列支持多规格价格区间(方案A)
- 需求:多规格商品各 SKU 价格不同,原列表价格列只显示主表 `price/salePrice/dealerPrice`(占位值),与 SKU 实际价格对不上。确认采用方案A:**多规格显示 ¥最低价 ~ ¥最高价,单规格保持单值**。
- 后端(**guilixu-java**,生效后端):
1. `entity/ShopGoods.java`:加 3 个**非持久化**字段 `priceMax / salePriceMax / dealerPriceMax`BigDecimal`@TableField(exist = false)`)。
2. `service/impl/ShopGoodsServiceImpl.java``pageRel`/`listRel` 查完后调 `fillSkuPriceRange(list)`——收集 `specs==1` 的商品,`shopGoodsSkuService.lambdaQuery().in(goodsId, ids)` 一次批量查 SKU,按 `price/salePrice/buyingPrice` 算 min/max 回填(主表 `price=min``priceMax=max`;市场价/会员价同理)。只填内存对象、不落库。
- 前端(**guilixu-admin**):
1. `api/shop/shopGoods/model/index.ts`ShopGoods 加 `priceMax? / salePriceMax? / dealerPriceMax?`
2. `views/shop/shopGoods/index.vue`priceGroup 列渲染改为「区间不相等时 ¥min ~ ¥max,否则单值」;导出 Excel 新增 `fmtPrice()` 同步处理。
- 验证:`guilixu-java` `mvn -o compile` 通过(本机无全局 mvn,临时用 /tmp/apache-maven-3.9.6 离线编译);`guilixu-admin` `vite build` 通过。
- 待确认:**mp-java 是否需要同步**——本部署 admin 商城走 guilixu-javamp-java 未参与;若 mp-java 侧也有管理端/列表调用 ShopGoods.listRel,则需同步 `fillSkuPriceRange` + 实体字段。
## 修复:商品「是否可以快递配送 / 运费模板」保存不成功(同根因)
- **根因**`ShopGoods` 实体(guilixu-java)与 `shop_goods` 表**都没有** `can_express` / `express_template_id` 字段。前端提交这两个值被 MyBatis-Plus 静默丢弃(与 8-11 多规格 buyingPrice 同一类问题)。
- **改动**
1. **guilixu-java** `entity/ShopGoods.java`:加 `private Integer canExpress;`(是否可以快递配送 0/1+ `private Integer expressTemplateId;`(运费模板ID,关联 shop_express_template.id)。`map-underscore-to-camel-case=true` 已开,自动映射 `can_express`/`express_template_id`
2. **迁移 SQL**(新建)`guilixu-java/sql/add_shop_goods_express_fields.sql``ALTER TABLE shop_goods ADD can_express TINYINT NOT NULL DEFAULT 1, ADD express_template_id INT NULL;`
3. **前端** `shopGoodsEdit.vue`:把"运费模板"表单项从「基本信息」页签移到「营销设置」页签内、"是否可以快递配送"开关**正下方**(用户要求)。
- **⚠️ 上线前置动作(否则不生效)**:必须在 db_guilixu 执行该 SQL 加列,且 guilixu-java 需重新编译部署(实体加了字段)。
- 此修复同时解决"运费模板"也存不进库的问题(同一根因,用户只报了 canExpress)。
- 前端 运费模板 必填校验(save 内 `canExpress===1 && !expressTemplateId`)与动态 label 已早先存在,逻辑无需改。
## 商品管理编辑弹窗:Markdown 编辑器换成 WangEditor 富文本
### 需求
用户反馈商品「编辑弹窗」的 Markdown 编辑器不好用,要求换成 website-admin 的 `app/pages/articles.vue` 那套编辑器。
### 方案(经评估后采用)
- articles.vue 用的是 `app/components/RichTextEditor.vue`WangEditor `@wangeditor/editor-for-vue`,输出 HTML),并非 Markdown。
- 采用「编辑时按需转换」策略:**老商品 content 是 Markdown,编辑回显时用 markdown-it 转成 HTML 再进富文本编辑器;已是 HTML 则原样返回**。零 DB 风险,存储格式新旧混存。
### 改动清单
1. **新增依赖**guilixu-adminpnpm):`@wangeditor/editor@^5``@wangeditor/editor-for-vue@^5``markdown-it@^14``@types/markdown-it@^14`(已写 package.json + node_modules)。
2. **新建组件** `src/components/RichTextEditor.vue`:从 website-admin 的同名组件复制,**删除 Nuxt 专属 `<ClientOnly>` 包装**guilixu-admin 是 Vite SPA 无此组件);图片上传用 `@/api/system/file``uploadFile`(返回 `res.url || res.path`admin 已有)。
3. **改造** `src/views/shop/shopGoods/components/shopGoodsEdit.vue`
- 移除 `MdEditor``md-editor-v3`+ 相关 import / `markdownToolbars` / `onMarkdownUploadImg` / `editorVisible` watch / `disabled` ref / `editorRef` / 文件库 `SelectData` 弹窗(无打开入口,死代码)/ `:deep(.md-editor-*)` 样式。
- 商品详情 tab 内改用 `<RichTextEditor v-model="content" :height="520" v-if="active==='content'"/>`(仅在 tab 激活时挂载,规避隐藏容器初始化宽度 0 问题)。
- 新增 `md = markdownit()``toEditorContent(raw)`,编辑回显 `content.value = toEditorContent(props.data.content)`
- 补回被误删的 `FileRecord` importchooseImage 等函数仍用)。
4. **保存逻辑不变**`save()` 已读 `content.value`,直接提交 HTML。
### 验证
- `pnpm build`vite build)通过,exit 0dist 正常产出。
### 风险 / 待确认
- **消费端(小程序 `guilixu-taro/src/pages/shop/product-detail.tsx`)已兼容两种格式**`parseContent``![alt](url)``<img>`,其余 HTML 直接 `RichText nodes` 渲染 → 新旧 content 都能展示,不崩。
- **潜在坑**:若 HTML content 里含裸 `.jpg` 链接(非 `<img>` 包裹,如 `<a href="https://x/x.jpg">`),`parseContent` 的兜底会把该 URL 误转成 `<img>` 破坏链接。WangEditor 产出的图片都是 `<img>`,正常商品详情极少触发,低概率。
- `uploadFile` 返回的 `url` 是否为小程序域名白名单内完整 URL 需上线后在真机确认(影响商品详情页图片显示)。
- 老商品仅在「重新编辑」时转 HTML;从未编辑过的老 Markdown 商品保持 Markdown,仍由小程序正确渲染。
## 文章管理编辑弹窗:统一换成 WangEditor 富文本(双模式→单模式)
- 用户要求文章模块编辑器"也换了",与商品模块一致。经确认采用「统一 WangEditor 单模式」,**删除原有的 TinyMCE 富文本 + MdEditor Markdown 双模式切换**。
- 改动文件:`src/views/cms/cmsArticle/components/articleEdit.vue`(复用已建的 `src/components/RichTextEditor.vue`)。
- 删除:双模式 `v-if="editor==1/2"` 渲染分支、`editor` ref、`onEditorTypeChange/performEditorSwitch/convertContentFormat/onEditor` 切换逻辑、`markdownToolbars``onMarkdownUploadImg``openMarkdownImageSelector/openMarkdownVideoSelector``insertMarkdownText`、TinyMCE `config`(含 custom_image_selector/quick_upload/auto_format/toggle_indent 等自定义按钮 setup)、`.mce-content-body` 样式、`uploadOss` import(仅在删块中使用)。
- 新增/改写:
- 内容详情改用 `<RichTextEditor ref="editorRef" v-model="content" :height="500">`
- 编辑器下方按钮区:`📷从图片库选择`/`🎬从视频库选择`(调 `editorRef.value.insertHtml` 插入 img/video,复用 SelectData 弹窗)、`🎨一键排版`/`📐首行缩进`(直接操作 `content.value` HTML 字符串,底层 smartFormatContent/addIndent/removeIndent 纯函数保留)。
- `md = markdownit()` + `toEditorContent(raw)`:回显时 Markdown→HTML,已是 HTML 原样返回。
- `save()` 固定 `editor: 1`(统一 HTML)。
- 验证:`pnpm build` 通过(exit 0)。
- 风险/待办:
- `src/views/cms/cmsWebsiteSettingEdit.vue` 仍有"编辑器类型 1 md-editor-v3, 2 tinymce-editor"设置字段,文章编辑已不再读取,**该设置现在对文章编辑无效**,建议后续清理(改动涉及系统设置暂未动)。
- 展示端 `guilixu-taro/src/pages/index/article-detail.tsx` 用 Taro `RichText` 渲染 HTML(目前是占位页),统一 HTML 安全。
- `uploadFile` 的 url 域名白名单同商品模块,需真机确认。
+25
View File
@@ -0,0 +1,25 @@
# 2026-08-12 工作记录
## 偏远地区不发货 — 如何让远端用户无法下单(tenant 10606
- 用户已 `UPDATE shop_goods set express_template_id=11 WHERE express_template_id IS NULL and tenant_id=10606`,以为这就禁了偏远地区下单。实际结论:**这条只绑运费模板(算运费用),没有生成「不发货黑名单」数据,不会拦截下单**。
- 后端拦截已端到端接通:`OrderBusinessService.createOrder``validateDeliveryRegionIfNeeded`(第87行) → `DeliveryRegionChecker`(按 tenant_id 查 `shop_express_template_detail` 中 region_mode=2 且 status=0 的明细,收货地址行政区划码命中即抛"XX暂不配送,请更换收货地址")。
- 链路确认:guilixu-taro `BaseUrl=https://guilixu-api.websoft.top/api``TenantId=10606`,下单走 guilixu-java(db_guilixu);单租户下买家 tenantId=10606,校验能命中黑名单。✅
- **admin 前端明细编辑表单未暴露 regionMode=2(只 0/1**,故黑名单只能 SQL 直插 `shop_express_template_detail`(region_mode=2, region_ids=偏远省6位码逗号串, status=0, tenant_id=10606, template_id=11)。已给 INSERT 模板与省码清单。
- 注意:黑名单是**租户级、全店生效**(不区分绑定哪个模板);自提(deliveryType=1)/selfTakeMerchantId 下单跳过校验。
## 补上后台「不发货黑名单」UI 模式(已交付)
-`src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue`:新增「配送模式」三态 radio(0全国/1指定配送/2指定不发货)mode=2 复用 `RegionMultiSelect` 选地区,保存写 region_mode=2 + region_ids(城市码逗号串);回显按 region_mode 初始化。
-`src/views/shop/shopExpressTemplateDetail/index.vue`:「配送地区」列按 region_mode 显示「不发货:xxx / 配送:xxx / 默认全国」,城市码聚合为省名显示。
- 类型检查(vue-tsc)确认本次改动无新错误(唯一相关报错是预存的 `$router` 用法,原文件即有,未引入)。
- 结论:黑名单现可经后台 UI 配置;SQL 直插仍作为备选(二者后台存城市码、SQL 存省份码,后端比对为编码集合 contains,都生效、互不冲突)。
- 已生成插偏远八省黑名单完整 SQL`offline-remote-areas-blacklist-10606.sql`template_id=11, region_mode=2, region_ids=新疆650000/西藏540000/青海630000/宁夏640000/内蒙古150000/海南460000/黑龙江230000/甘肃620000, status=0, tenant_id=10606),含前置查重 + 后置验证语句。
## 微信支付报错 APPID_MCHID_NOT_MATCHtenant 10606 下单失败)
- 现象:用户下单报 `创建微信支付订单失败:Wrong HttpStatusCode[400] ... APPID_MCHID_NOT_MATCH`。服务商模式 JSAPI,请求体 sp_appid=`wx541db955e7a62709`/sp_mchid=`1720296183`、sub_appid=`wxcad054ee427aaca1`/sub_mchid=`1744894035`
- 根因:传给微信的 `sub_appid``sub_mchid` 在微信侧不匹配(该子商户应用ID未绑定到该子商户号)。代码无 bug。
- 4 个值来源:`gxwebsoft_core.sys_payment`tenant 10606 微信JSAPI记录)字段 `app_id`/`mch_id`/`sub_app_id`/`sub_mch_id`;拼装在 `ShopOrderServiceImpl.java:809-912`(服务商模式)。
- 已验证 `sub_app_id=wxcad054ee427aaca1` 就是 `guilixu-taro` 用户端小程序真实 appidproject.config.json),故 sub_appid 是对的、与买家 openid 一致。
- 结论:问题在 `sub_app_id↔sub_mchid` 绑定被破坏——最可能(1)商户平台子商户 `1744894035` 对 appid 的授权被移除/失效;(2)`sys_payment.sub_mch_id` 被误改。
- 修复:微信商户平台(服务商 `1720296183` 服务商平台→特约商户管理→开发配置)为子商户 `1744894035` 绑定 appid `wxcad054ee427aaca1`;并 `SELECT` 核对 `sys_payment` 的 sub_mch_id 仍为 `1744894035`。无需改代码,新订单号直接读最新配置。
+26
View File
@@ -0,0 +1,26 @@
# 2026-08-14 工作日志
## 修复商品分类层级(10409 → 10421
- 问题:guilixu-admin 后台 `/shop/shopGoodsCategory` 页面中,10409「二级分类」与 10421「特别专区」显示为同级。
- 原因:前端按 `parentId` 渲染树形层级,说明 10409 的 `parent_id` 不是 10421(实际可能为 0)。
- 处理:
- 确认后端表为 `shop_goods_category`guilixu-java / db_guilixu),关键字段 `category_id``parent_id`
- 生成修复 SQL`fix-category-parent-10409.sql`,将 10409 的 `parent_id` 更新为 10421。
- 同时提供了后台 UI 的临时操作方式:编辑 10409 把上级分类改为 10421。
- 文件:`/Users/gxwebsoft/VUE/guilixu-admin/fix-category-parent-10409.sql`
## 商品分类编辑弹窗新增「链接地址」字段
- 需求:在 `shopGoodsCategoryEdit.vue` 的「分类标识」下方新增「链接地址」输入项,存到 `path` 字段。
- 改动:`src/views/shop/shopGoodsCategory/components/shopGoodsCategoryEdit.vue`
- 在「分类标识」form-item 之后新增「链接地址」项,绑定 `form.path`(form 已有该字段,无需改 model/后端)。
- 重置逻辑 Object.assign 补上 `path: undefined`,保证新增时清空。
- 注意:仅前端 UI 改动,后端表 `shop_goods_category.path` 字段已存在,无需迁移。
## 商品分类列表树形连线高度调紧凑
- 问题:后台 `/shop/shopGoodsCategory` 列表中,树形层级连线 SVG 高度 40px,导致行显得太高、不紧凑。
- 改动:`src/views/shop/shopGoodsCategory/index.vue`
- `renderTreePrefix` 中 SVG 高度 `H``40` 改为 `28``mid` 自动为 `14`
- 效果:二级/三级分类前的树形引导线高度降低,整行更紧凑。
+23
View File
@@ -0,0 +1,23 @@
# 2026-08-15 工作日志
## 专区功能(ShopHomeSection)接口报错修复
- 现象:访问专区列表接口报 `NoSuchPropertyException: ShopHomeSectionParam.deleted`
- 根因:模板生成的 `ShopHomeSectionParam`guilixu-java `src/main/java/com/gxwebsoft/shop/param/ShopHomeSectionParam.java`**漏了 `deleted` 字段**;同项目 Mapper XML`ShopHomeSectionMapper.xml`)按模板带 `<if test="param.deleted == null"> AND a.deleted = 0</if>` 逻辑删除过滤,OGNL 求值时属性不存在即抛异常。
- 对比:`ShopGoodsParam``deleted` 字段(@QueryField EQ + @Schema),为标准写法;`ShopHomeSection` 实体本身有 `@TableLogic private Integer deleted;`,表启用逻辑删除。
- 修复:给 `ShopHomeSectionParam` 末尾补 `private Integer deleted;`(带 @QueryField + @Schema),与模板一致。
- 验证:重新编译部署 guilixu-java(端口 9200 本地实例)后接口恢复正常。
- ⚠️ 同类预警:其他新生成的 Param 若同样漏 `deleted`,会报一模一样的错,补字段即可。
## 专区功能菜单未显示(待办)
- 前端页面 `src/views/special/zone/index.vue` 与 API 层 `src/api/shop/shopZone/` 已完成。
- 但因 guilixu-admin 为动态路由(菜单由后端接口拉取),后端菜单表尚未插入「专区管理」记录,故左侧导航看不到入口。
- 待处理:在后端菜单表(sys_menu 类)插入菜单记录(component=special/zonetenant_id=10606),刷新/重登后生效。
## 专区功能交互入口约定(答疑结论)
- 白名单用户:专区列表每行「白名单」按钮 → 弹窗勾选用户 → 确定(PUT /{id}/users 覆盖式保存到 shop_home_section_user 表)。前端 UserSelectModal 链路完整。
- 商品关联:专区页「商品」按钮只是**查看**该专区下商品(GET /{id}/goods,后端用 FIND_IN_SET(section_id, section_ids) 查询);**往专区加商品不在此页操作**,要去「商品管理 → 编辑商品 → 所属专区(多选)」勾选专区并保存,写入 goods.section_ids。
- 后端接口齐全:users/goods 查询与设置、check-permission 权限校验均已实现。
- ⚠️ 待确认:白名单弹窗当前按 isStaff=true 查后台员工用户(@/api/system/user 的 pageUsers);若受限专区面向小程序买家,需调整用户来源。
+30
View File
@@ -0,0 +1,30 @@
# 2026-08-16 工作日志
## 专区白名单用户弹窗修复
### 1. 去掉 isStaff 限制
- 文件:`src/views/special/zone/components/UserSelectModal.vue`
- 改动:`pageUsers` 调用移除 `isStaff: true` 参数,使白名单搜索覆盖全部用户(不再只查后台员工)。
### 2. 修复勾选框不显示
- 现象:弹窗表格数据正常但无 checkbox 勾选列,用户无法选择。
- 根因:`rowSelection``reactive()` 包整个对象的方式在 `ele-pro-table` 中不生效;且缺少 `columnWidth` 导致勾选列无宽度不渲染。
- 对比:项目内 `shopCoupon/index.vue``computed(() => ({ columnWidth, selectedRowKeys, onChange }))` 模式可正常工作;字典页用 `{ columnWidth: 32 }` 最简写法也可。
- 修复:
- `reactive``computed` 返回 rowSelection 对象(与 shopCoupon 一致)
-`columnWidth: 48`
- `selectedRowKeys` 独立为 `ref<number[]>`,通过 computed 注入 rowSelection
- 所有引用点同步更新(模板计数、confirm、watch 初始化)
## 专区商品抽屉改造为可编辑
- 需求:专区「商品」按钮打开的抽屉从只读列表升级为支持添加/移除商品。
- 后端(guilixu-java `ShopHomeSectionController.java`)新增两个接口:
- `POST /{id}/goods` — 添加商品到专区(接收 goodsId[],将 section_id 追加到每个 goods 的 section_ids 逗号串中,用 LinkedHashSet 去重)
- `DELETE /{id}/goods` — 从专区移除商品(接收 goodsId[],从 section_ids 中移除该 section_id,空则置 null
- 前端 API`src/api/shop/shopZone/index.ts`)新增 `addSectionGoods``removeSectionGoods`
- 前端 UI`src/views/special/zone/index.vue`)商品抽屉改造:
- 顶部:搜索框 + 「搜索」按钮 + 「全部添加(N)」确认按钮
- 搜索结果区:虚线框展示候选商品列表,每行有「+添加」单条操作(自动过滤已在专区内的商品)
- 已关联商品表格:每行加「移除」按钮(二次确认后调用 remove 接口)
- 数据源:搜索商品复用已有 `pageShopGoods` API@/api/shop/shopGoods
+32
View File
@@ -0,0 +1,32 @@
# 2026-08-17 工作日志
## 专区销量统计功能(/special/zone
- 调研:专区与商品靠 `ShopGoods.section_ids`(逗号串)关联;`ShopOrderGoods` 无 sectionId,有 `goodsId/totalNum/price/payStatus/orderStatus/tenantId`;商品有 `sales` 累计字段。
- 确认方案:方案B(订单真实成交聚合)+ 列表汇总列 + 独立统计弹窗,指标=销量件数+销售额。
- 后端 guilixu-javashop 模块)改动:
- ShopHomeSection 实体加 salesNum/salesAmount@TableField(exist=false)
- 新建 VOSectionSalesSummaryVO / ShopSectionGoodsRankVO / SectionSalesStatsVOvo 包)
- ShopHomeSectionMapper 加 selectSectionSalesSummary(批量)/ selectSectionGoodsRankTOP50
- ShopHomeSectionServiceImpl.pageRel 批量填充汇总 + 新增 getSectionSalesStats
- ShopHomeSectionController 新增 GET /{id}/stats?start=&end=
- 口径:payStatus=1 且 orderStatus NOT IN(2,6)tenantId 隔离,时间对应 create_time
- 前端 guilixu-admin
- shopZone API 加 getSectionSalesStatsmodel 加字段与 VO 类型
- zone/index.vue 列表加「销量件数」「销售额」列 + 操作列「销量」按钮
- 新建 SectionSalesStatsModal.vue(时间筛选 + 汇总卡片 + 商品排行)
- vite build 通过(exit 0
- 待办:后端需 maven 编译并部署到 guilixu-api.websoft.top 才生效(本环境未部署)。
## 修复专区商品抽屉分页点击无效
- 文件:`src/views/special/zone/index.vue`
- 问题:抽屉里 `a-table``:pagination` 直接内联写 `{ current: goodsPage, pageSize: goodsPageSize, total: goodsTotal }`,对象属性中的 ref 不会被 Vue 自动解包,Pagination 拿到的是 ref 对象而非 number,导致分页按钮点不动。
- 修复:
- 提取 `goodsPagination = computed(() => ({ total: goodsTotal.value, current: goodsPage.value, pageSize: goodsPageSize.value, showSizeChanger: true, onChange: ... }))`
- `:pagination="goodsPagination"`
- `onChange``loadGoods(currentSectionId.value)` 修正为 `.value`
- 验证:`vite build` 通过(exit 0)。
## 专区商品图片走压缩 URL
- 文件:`src/views/special/zone/index.vue`
- 改动:专区商品抽屉的已关联商品列表「图片」列,从直接 `:src="text"` 改为 `:src="getCompressedImageUrl(text, { width: 120 })"`,缩小缩略图体积并统一走 OSS 图片处理。
- 说明:`getCompressedImageUrl` 已导入,无需额外引入。
+40
View File
@@ -0,0 +1,40 @@
# 2026-08-18 工作日志
## 专区商城价 = 市场价 × 折扣系数 功能(已实现)
客户要求:专区的商城价显示为市场价的 0.78(并按此价成交)。已与用户确认:展示+成交价都按系数、每专区单独配(默认1.00)、基数=市场价 salePrice。
### 后端 guilixu-java(仅此,商城走 guilixu-java
- `ShopHomeSection` + `ShopHomeSectionParam` 增加 `discountRate`(BigDecimal, 默认1.00)。
- 迁移 SQL`guilixu-admin/zone-discount-rate-10606.sql``ALTER TABLE shop_home_section ADD discount_rate DECIMAL(10,2) NOT NULL DEFAULT 1.00`)。需在生产库执行。
- `OrderCreateRequest.OrderGoodsItem` 增加 `sectionId`(每商品项携带专区ID)。
- `OrderBusinessService` **两处**价格解析(①`validateAndCalculateTotal` 算总额 ②`createOrder` 落库订单商品价)都加了专区折扣逻辑:若 `item.sectionId` 命中且商品 `section_ids` 含它、且专区 `discountRate≠1`,则 `actualPrice = goods.getSalePrice() × rate`(保留两位小数,HALF_UP)。后端不信任前端传价,按 sectionId 重算,防篡改。
### 后台 guilixu-admin
- `src/api/shop/shopZone/model/index.ts``HomeSection` 增加 `discountRate?: number`
- `src/views/special/zone/components/zoneEdit.vue`:表单加「折扣系数」输入框(a-input-number0.01~1,默认1,提示"专区商城价=市场价×系数"),表单默认值 `discountRate: 1`
### 小程序 guilixu-taro(仅「专区→立即购买」链路打通)
- `src/api/shop/shopHomeSection/index.ts` 增加 `getHomeSection(id)`
- `src/api/shop/shopHomeSection/model/index.ts``HomeSection` 增加 `discountRate?`
- `section-detail.tsx`:进入时 `getHomeSection` 取折扣系数并展示(价格=市场价×系数,划线价显示市场价),跳转详情带 `sectionId`+`discountRate`
- `product-detail.tsx`:读 `sectionId`/`discountRate`,按系数展示;立即购买时把系数写入 `buy_now` 存储并带参跳结算。
- `src/api/shop/shopOrder/model/index.ts``OrderGoodsItem` 增加 `sectionId?`
- `checkout.tsx`:增加 `getItemDisplayPrice`/`getItemSectionId`,按系数估价并随下单提交 `sectionId`
### ⚠️ 已知限制
- **购物车链路不支持专区折扣**:服务端 `shop_cart` 表无 `sectionId` 字段,从专区「加入购物车」再结算会走原价(仅「立即购买」享受折扣)。若要购物车也生效,需给 `shop_cart` 表加 `section_id` 并在下单时透传——后续需求。
- 未执行迁移 SQL、未重新部署后端、未构建发布前端,需按环境操作。
### 验证
- 前端 `tsc --noEmit` 因环境配置(taro 缺 @tarojs/taro 类型、admin 大量既存错误)未完整跑通,但本次改动文件未产生任何新报错。
- 后端 Maven 不在本环境,未编译;改动为标准 Java,已逐行核对。
## 购物车链路打通专区折扣(补充实现)
用户要求把"购物车也支持专区折扣"补上。在「立即购买」已实现基础上,打通购物车链路:
- **后端 guilixu-java**`ShopCart` 实体加 `sectionId`(真实列)+`discountRate`(exist=false 关联字段)`ShopCartMapper.xml` 的 selectSql 增加 `LEFT JOIN shop_home_section hs ON a.section_id = hs.section_id` 并选 `hs.discount_rate AS discount_rate`section_id 为 NULL 时不影响普通商品);`ShopCartServiceImpl.addToCart` 在"已存在累加"分支若本次新传 sectionId 非空则刷新 `existCart.sectionId`(让专区加购覆盖普通加购的 null)。迁移SQL:`zone-cart-section-id-10606.sql``ALTER TABLE shop_cart ADD section_id INT NULL`)。
- **小程序 guilixu-taro**`CartContext.addItem` 增可选 `sectionId` 参数并透传给加购接口;`refresh` 映射带 `sectionId`/`discountRate``CartItem` 接口加两字段;`calcPrice``discountRate≠1` 时用 `salePrice×rate``section-detail`/`product-detail` 加购调用传 `sectionId``cart.tsx` 单价展示用折扣价;`checkout.tsx``getItemDisplayPrice` 折扣分支改读顶层 `salePrice`(购物车项 product 为空,否则算不出折扣价)。
- 下单成交价仍由 `OrderBusinessService``sectionId` 重算,购物车项带 sectionId 后自动生效,无需改下单逻辑。
- 验证:taro `tsc --noEmit` 对本次改动 5 个文件零新报错。
+12
View File
@@ -0,0 +1,12 @@
# 项目长期记忆
## 专区折扣系数功能(2026-08-18 新增)
- 需求:专区商城价 = 市场价(salePrice) × discountRate(每专区可配,默认 1.00;本专区用 0.78)。
- **后端 guilixu-java**`ShopHomeSection.discountRate` 字段 + 迁移SQL(`guilixu-admin/zone-discount-rate-10606.sql`)`OrderCreateRequest.OrderGoodsItem.sectionId``OrderBusinessService` 两处价格解析按 sectionId 重算成交价= salePrice×rate(后端算,不信任前端价)。
- **后台 guilixu-admin**`zoneEdit.vue` 有「折扣系数」输入;`shopZone/model` HomeSection 有 `discountRate`
- **小程序 guilixu-taro**`section-detail``product-detail``checkout` 透传 sectionId+discountRate,「立即购买」与「购物车」两条链路均按系数生效(购物车补充见下)。
- **购物车链路补充(同日)**`shop_cart` 表加 `section_id` 列(迁移SQL见 `zone-cart-section-id-10606.sql`);`ShopCart` 实体加 `sectionId`(真实列)+`discountRate`(exist=false)、`ShopCartMapper.xml` LEFT JOIN 专区取 `discount_rate``addToCart` 在"已存在累加"分支若新传 sectionId 非空则刷新 `existCart.sectionId`(专区加购覆盖普通加购的 null);`CartContext.addItem` 透传 sectionId、`calcPrice``cart.tsx` 按系数展示;`checkout.tsx``getItemDisplayPrice` 折扣分支改读顶层 `salePrice`(购物车项 product 为空)。两条链路现在都生效。
- 生效前置:执行迁移SQL、重新部署 guilixu-java、构建发布 taro。
## 双后端事实(沿用)
- 商城接口实际调 `https://guilixu-api.websoft.top`(= guilixu-java, db_guilixu)cms 走 mp-java。改商城相关先确认运行时 ApiUrl。详见历史 MEMORY。
+79 -62
View File
@@ -1,84 +1,89 @@
<div align="center">
<h1>🌐 WebSoftAdmin</h1>
<p><strong>基于 Vue 3 + Ant Design Vue 的企业级后台管理系统</strong></p>
<h1>🛒 小程序商城后台管理系统</h1>
<p><strong>基于 Vue 3 + Ant Design Vue 的企业级小程序商城后台</strong></p>
<p>
<img src="https://img.shields.io/badge/Vue-3.x-4FC08D" alt="Vue">
<img src="https://img.shields.io/badge/Ant%20Design%20Vue-3.x-1890FF" alt="Ant Design Vue">
<img src="https://img.shields.io/badge/TypeScript-4.x-3178C6" alt="TypeScript">
<img src="https://img.shields.io/badge/Vite-4.x-646CFF" alt="Vite">
<img src="https://img.shields.io/badge/Ant%20Design%20Vue-4.x-1890FF" alt="Ant Design Vue">
<img src="https://img.shields.io/badge/TypeScript-5.x-3178C6" alt="TypeScript">
<img src="https://img.shields.io/badge/Vite-8.x-646CFF" alt="Vite">
<img src="https://img.shields.io/badge/License-MIT-blue" alt="License">
</p>
</div>
## 📖 项目简介
WebSoftAdmin 是一个基于 **Vue 3 + Ant Design Vue** 构建的现代化企业级后台管理系统,采用最新的前端技术栈:
小程序商城后台管理系统是一个基于 **Vue 3 + Ant Design Vue** 构建的现代化企业级商城管理平台,为小程序商城提供完整的后台管理能力,涵盖商品、订单、会员、经销商、门店、营销活动等核心业务模块。
- **前端框架**Vue 3 + TypeScript + Vite
- **UI 组件库**Ant Design Vue 3.x
- **富文本编辑器**TinyMCE(支持图片/视频上传、一键排版)
- **图表库**ECharts + G2
- **工具库**Lodash、Day.js、CryptoJS
## 项目演示
| 后台管理系统 | https://mp.websoft.top |
|--------|-------------------------------------------------------------------------------------------------------------------------------------|
| 账号密码 | [立即注册](https://mp.websoft.top/register/?inviteCode=github) |
| 关注公众号 | ![输入图片说明](https://oss.wsdns.cn/20240327/f1175cc5aae741d3af05484747270bd5.jpeg?x-oss-process=image/resize,m_fixed,w_150/quality,Q_90) |
- **UI 组件库**Ant Design Vue 4.x + EleAdmin Pro
- **状态管理**Pinia
- **图表库**ECharts + @antv/g2
- **工具库**Lodash、Day.js、CryptoJS、ExcelJS
**项目域名**[https://shop.websoft.top](https://shop.websoft.top)
**官方产品介绍**[https://websoft.top/shop](https://websoft.top/shop)
## 🛠️ 技术栈
### 核心技术
| 技术 | 版本 | 说明 |
|------|------|------|
| Vue | 3.x | 渐进式 JavaScript 框架 |
| TypeScript | 4.x | JavaScript 的超集 |
| Vite | 4.x | 下一代前端构建工具 |
| Ant Design Vue | 3.2.11 | 企业级 UI 设计语言 |
| EleAdmin Pro | 1.10.1 | 企业级组件库 |
| TypeScript | 5.x | JavaScript 的超集 |
| Vite | 8.x | 下一代前端构建工具 |
| Ant Design Vue | 4.x | 企业级 UI 设计语言 |
| EleAdmin Pro | 1.11+ | 企业级组件库 |
| Pinia | 2.x | Vue 3 状态管理库 |
### 功能组件
- **TinyMCE** - 富文本编辑器,支持图片/视频上传
- **ECharts** - 数据可视化图表库
- **CropperJS** - 图片裁剪组件
- **ExcelJS** - Excel 文件处理
- **ExcelJS / xlsx** - Excel 文件导入导出
- **Ali OSS** - 阿里云对象存储
- **qrcode / jsbarcode** - 二维码与条形码生成
## 📋 环境要求
### 基础环境
- 🟢 **Node.js 16+**
- 📦 **npm 8+ / yarn 1.22+**
- 🟢 **Node.js 18+**
- 📦 **npm 8+ / yarn 1.22+ / pnpm 8+**
- 🌐 **现代浏览器**Chrome 63+、Firefox、Safari、Edge
### 开发工具
- **推荐**VS Code / WebStorm
- **插件**Vetur / VolarVue 3 支持)
- **插件**VolarVue 3 支持)
## 🚀 快速开始
### 1. 克隆项目
```bash
git clone https://github.com/websoft-top/paopao-vue.git
cd paopao-vue
git clone <仓库地址>
cd guilixu-admin
```
### 2. 安装依赖
```bash
# 使用 npm
npm install
# 或使用 yarn
yarn install
# 或使用 pnpm(推荐)
pnpm install
```
### 3. 配置环境变量
```bash
# 复制环境变量示例文件
cp .env.example .env
@@ -88,6 +93,7 @@ cp .env.example .env
```
### 4. 启动开发服务器
```bash
# 开发模式
npm run dev
@@ -99,6 +105,7 @@ yarn dev
访问 `http://localhost:3000` 即可看到管理后台。
### 5. 构建生产版本
```bash
# 生产构建
npm run build
@@ -112,6 +119,7 @@ npm run serve
项目使用环境变量来管理敏感信息和配置。请按照以下步骤配置:
### 必需配置
```bash
# API 配置
VITE_API_URL=https://your-api.com/api # 后端 API 地址
@@ -124,51 +132,54 @@ VITE_TENANT_ID=your_tenant_id # 租户 ID
```
### 可选配置
```bash
# 高德地图 (如需使用地图功能)
VITE_MAP_KEY=your_map_key # 高德地图 Key
VITE_MAP_CODE=your_map_security_code # 高德地图安全密钥
# WebSoftAdmin 授权 (商业版功能)
# EleAdmin Pro 授权 (商业版功能)
VITE_LICENSE_CODE=your_license_code # 授权码
```
### 获取配置信息
- **高德地图密钥**:访问 [高德开放平台](https://lbs.amap.com/) 申请
- **WebSoftAdmin 授权码**:联系 [官方网站](https://websoft.top/) 获取
- **其他 API 配置**:根据您的后端服务配置
## 🎯 核心功能
### 📝 内容管理系统
- **文章管理**:支持富文本编辑、图片/视频上传
- **一键排版**:智能文章格式优化,包含10种专业排版样式
- **媒体库**:图片/视频文件管理,支持分组和搜索
- **首行缩进**:中文段落格式智能切换
### 🛒 商城管理
- **商品管理**:商品信息编辑、规格设置
- **订单管理**订单流程跟踪
- **商品管理**商品信息编辑、规格设置、SKU 管理、商品分类
- **订单管理**:订单流程跟踪、订单商品管理
- **库存管理**:商品库存监控
- **优惠券管理**:优惠券创建与发放、用户优惠券管理
- **营销活动**:活动管理、活动报名、积分商品
- **礼品管理**:商城礼品配置
### 👥 用户权限
- **用户管理**:用户信息维护
- **角色权限**:基于角色的访问控制
### 🏪 门店与经销商
- **门店管理**:门店信息、门店围栏、门店骑手、门店仓库
- **经销商体系**:经销商申请、经销商用户、经销商订单、经销商资金与提现
- **分销设置**:经销商推荐人、分销海报、分销配置
### 👥 会员管理
- **会员注册**:会员注册管理
- **会员权益**:权益包管理、权益兑换
- **会员推荐**:推荐人关系管理
### 👤 用户权限
- **用户管理**:用户信息维护、用户分组
- **角色权限**:基于角色的访问控制(RBAC)
- **菜单管理**:动态菜单配置
- **组织架构**:组织与部门管理
## 🎨 富文本编辑器特色功能
### ⚙️ 系统管理
### 📸 媒体上传
- **图片上传**:支持拖拽、粘贴、文件选择
- **视频上传**:支持多种视频格式
- **媒体库**:统一的媒体文件管
- **OSS 存储**:阿里云对象存储集成
### ✨ 智能排版
- **一键排版**:10种专业排版优化
- **首行缩进**:中文段落格式切换
- **样式优化**:标题、段落、列表、表格等元素美化
- **响应式**:适配不同屏幕尺寸
- **系统配置**:商城设置、提现配置
- **支付管理**:支付配置、订单支付
- **字典管理**:数据字典维护
- **缓存管理**:缓存清
- **域名管理**:域名白名单配置
- **操作记录**:登录记录、操作日志
## 🏗️ 项目结构
@@ -176,11 +187,17 @@ VITE_LICENSE_CODE=your_license_code # 授权码
src/
├── components/ # 公共组件
├── views/ # 页面组件
│ ├── cms/ # 内容管理
│ ├── shop/ # 商城管理
── system/ # 系统管理
│ ├── shop/ # 商城管理(商品、订单、门店、经销商、会员等)
│ ├── system/ # 系统管理(用户、角色、菜单、字典、支付等)
── passport/ # 登录认证
│ ├── user/ # 用户中心
│ └── result/ # 结果页
├── router/ # 路由配置
├── store/ # 状态管理
├── store/ # Pinia 状态管理
├── utils/ # 工具函数
└── assets/ # 静态资源
```
## 📄 License
[MIT](LICENSE)
+35
View File
@@ -0,0 +1,35 @@
-- ============================================================
-- 商品表新增「置顶」字段 is_top,并调整默认排序(方案 B)
-- ------------------------------------------------------------
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
-- 1. is_top = 1 表示置顶,0(默认)表示不置顶
-- 2. 表名/字段名 snake_case 约定(Node + TypeORM 常见);
-- 若你的库列名是驼峰(goodsId / isTop),请自行替换
-- 3. 与 recommend(推荐)同为「列表内联一键切换」的布尔开关
-- 4. 混合排序思路:置顶的永远在最前;置顶内部按排序号升序;
-- 其余商品按创建时间倒序(绝大多数商品无需手动排)
-- ============================================================
-- 第一步:新增字段(已存在则跳过;MySQL 5.7 / 8 通用)
ALTER TABLE shop_goods
ADD COLUMN is_top TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶:1置顶 0不置顶';
-- (可选)联合索引,置顶 + 排序号分页查询更稳
-- ALTER TABLE shop_goods
-- ADD INDEX idx_is_top_sort (is_top, sort_number);
-- 第二步:调整分页查询(page 接口)默认排序
-- 原(示例):ORDER BY create_time DESC
-- 改: ORDER BY is_top DESC, sort_number ASC, create_time DESC
-- 说明:若后端排序字段来自前端 orders 参数,需放开 is_top / sort_number 参与排序
-- (前端「置顶」「排序号」列已开启 sorter,会向后端传 orders
-- 第三步(验证):确认字段已加、数据默认 0,且排序符合预期
SELECT goods_id,
name,
is_top,
sort_number,
create_time
FROM shop_goods
ORDER BY is_top DESC, sort_number ASC, create_time DESC
LIMIT 20;
+73
View File
@@ -0,0 +1,73 @@
# 桂礼序 CMS 模块复用方案分析
> 分析对象:`mp-java`modules 服务,cms 235 类) vs `guilixu-java`(桂礼序主后端,cms 152 类)
> 分析日期:2026-07-25
## 一、现状事实(已核实)
| 维度 | mp-java | guilixu-java |
|------|---------|--------------|
| 形态 | 单体 Spring Boot 应用(单 pom@MapperScan `com.gxwebsoft.**` | 同左 |
| cms 类数 | **235**controller 37 | **152**controller 25 |
| cms 数据库 | `modules` 库 | `db_guilixu`prod/dev)、`modules`test |
| cms 建表/迁移 | 由 `src/main/java/com/gxwebsoft/cms/sql/` + `docs/sql` 管理,高频更新 | `sql/` 目录**无 cms 建表脚本** |
| git 引入时间 | 2025-07-24"2.0版本分离"),持续开发 | 2026-06-01"桂礼序"),已落后 ~1.5 个月 |
| 最近提交 | 2026-07-24/25(仍在改 cms | 最近为 shop/order 相关,cms 停滞 |
**关键证据:**
1. **guilixu-java 的 152 个 cms 类,全部能在 mp-java 的 cms 里找到同名类**mp-java 额外多出 83 个(CmsApp / CmsBanner / CmsCase 等系列)。→ guilixu 的 cms 是 mp-java cms 的**子集分叉**。
2. **前端(guilixu-admin)的 cms 接口早已使用 `MODULES_API_URL`**,路径如 `MODULES_API_URL + '/cms/cms-ad/page'``MODULES_API_URL` = `localStorage.ApiUrl || VITE_API_URL`,即指向 modules 服务(mp-java)。shop 的商户/优惠券接口同样走 `MODULES_API_URL`。→ **前端本就假定 cms/shop 由 mp-java 提供,guilixu-java 自己的 cms 包前端并不调用(重复/死代码)。**
3. **mp-java 与 guilixu-java 的 cms 控制器路径完全一致**`/api/cms/cms-ad``/api/cms/cms-article-like` …),说明两者是同一套接口契约。
4. **完整复制不可编译**mp-java 完整 cms 的 `CmsApp*`/`CmsWebsiteService*` 依赖 `com.gxwebsoft.project`Project/ProjectService),而 guilixu-java **没有 project 模块**。把 235 类整体搬过去会编译失败。
## 二、方案对比
### 方案 Aguilixu 直接调用 mp-java 的 cmsmodules 服务作为 cms 提供方)
**优点**
- ✅ 单一事实来源:cms 逻辑只维护一份,guilixu 永远拿到最新功能(含 83 个新增类)。
- ✅ 前端几乎零改动:guilixu-admin 的 cms 接口**已经**指向 `MODULES_API_URL`,契约路径与 mp-java 一致。
- ✅ 消除重复:可删除 guilixu-java 里 152 个冗余 cms 类,降低维护与同步负担。
- ✅ cms 数据/建表/迁移由 mp-java 集中管理(modules 库),guilixu 无需自建 cms 表。
- ✅ 符合现有"主后端 + modules 服务"双基地址架构(shop 已实现)。
**缺点 / 风险**
- ⚠️ 运行期耦合:guilixu 的 cms 功能可用性依赖 mp-java 在线(mp-java 宕机/升级 → cms 不可用)。
- ⚠️ 跨库一致:cms 在 `modules`guilixu 业务在 `db_guilixu`;若需"同事务写 cms+shop"无法用本地事务,需走事件/Saga。
- ⚠️ 鉴权边界:mp-java 与 guilixu 各自用户体系(不同库),需确保 modules 服务对 guilixu-admin 的 cms 调用有合法鉴权(现有 TENANT_ID/APP_SECRET/ApiUrl 机制应已覆盖)。
- ⚠️ 接口微小差异:guilixu 是旧分叉,个别字段/路径若 mp-java 已重构,需对一遍(如 CmsWebsite 已删手机号脱敏方法)。
### 方案 B:把 cms 模块整个复制到 guilixu-java
**优点**
- ✅ 自包含:cms 随 guilixu-java 独立运行,不依赖 mp-java 在线。
- ✅ 接口契约稳定:沿用现有 guilixu-java cms 路径,前端无需改 baseURL。
- ✅ 可做 guilixu 专属深度定制,不受 mp-java 限制。
**缺点 / 风险**
-**语义上"整个复制"不可行**:完整 235 类依赖 guilixu 没有的 `project` 模块,编译失败;只能裁剪到子集,等于失去"完整"意义。
- ❌ 固化分叉:现在已落后 83 类/1.5 个月;mp-java cms 仍高频演进,复制后会陷入"复制→分叉→再复制"循环,双份维护成本最高。
- ❌ 数据迁移:需把 cms 表从 `modules` 迁到 `db_guilixu`,并自补建表/迁移脚本(目前 guilixu 无 cms 建表脚本)。
- ❌ 与前端现状冲突:前端 cms 调的是 `MODULES_API_URL`mp-java),复制后这套 guilixu cms 实际无人调用(死代码),反而制造两套真相。
### (补充)方案 C:抽独立 cms 共享库/微服务(长期最优,成本高)
把 cms 抽成 `cms-spring-boot-starter`(或独立 cms 微服务 + 客户端 SDK),mp-java 与 guilixu-java 都依赖。能根治重复与分叉,但需重构打包、版本发布流程,短期落地成本高于 A。
## 三、结论与落地建议
**结论:选方案 Aguilixu 直接调用 mp-java 的 cms),不要整体复制到 guilixu-java。**
理由(按重要性):
1. 前端早已按"modules 服务(mp-java)提供 cms"设计(cms 接口全走 `MODULES_API_URL`,路径契约一致)——方案 A 是顺延既有架构,方案 B 反而与现状冲突。
2. guilixu 的 cms 是 mp-java 的旧分叉子集且前端不调它,复制/扩展只会把分叉永久化;"整体复制"还因缺 `project` 模块无法编译。
3. cms 数据、建表、迁移本就在 mp-java(modules 库)集中管理,远程调用天然复用。
**落地步骤建议:**
1. 确认 `guilixu-admin``MODULES_API_URL` / `localStorage.ApiUrl` 指向 mp-java 部署实例(modules 服务),cms 调用即生效(前端基本不用动)。
2. **删除 guilixu-java 中重复的 `com/gxwebsoft/cms` 包(152 类)**,消除死代码与未来同步负担。
3. 对一遍 mp-java 与 guilixu 旧 cms 的接口字段差异(重点 CmsWebsite 等已重构类),确保前端无误。
4. 明确可用性边界:在运维/部署文档标注"cms 依赖 modules 服务",并为 mp-java 配置高可用/健康检查。
5. 若后续确实需要 guilixu 专属、与自身业务同事务的 cms 能力,再走方案 C(抽共享库),不要走方案 B。
> 备注:若你的真实诉求是"让 guilixu 完全独立、不依赖 mp-java 在线",则需在方案 B 基础上额外补 `project` 模块依赖 + cms 表迁移到 `db_guilixu` + 长期同步机制,综合成本最高,除非有强隔离需求否则不推荐。
@@ -0,0 +1,129 @@
# 运费模板「不发货地区(排除地区)」接口方案
> 目标:支持商家配置"除部分偏远地区外,全国均可配送"的场景,避免逐个添加可配送省份。
> 前端代码库:guilixu-adminshop 模块接口走 MODULES_API_URL,即 mp-java 的 shop 模块)
---
## 一、现状(改造前)
- 一条 `shop_express_template_detail` = 一种配送区域 + 一套运费规则。
- 当前**只能选 1 个省市组合**,字段为 `province_id` + `city_id`(单值)。
- 不选 = 默认全国。
- **无任何排除/不发货逻辑**。
## 二、需求(改造后)
配送区域支持两种模式:
| 模式 | 含义 | 典型场景 |
|------|------|----------|
| 白名单(指定配送) | 只发列表中的地区 | 仅发江浙沪 |
| 黑名单(指定不发货) | 发列表外的所有地区 | 除新疆/西藏/港澳台外全国发 |
新增「不发货地区」配置:商家框选不发货的省/市,保存后这些地区下单时报"暂不配送"。
## 三、数据库变更
`shop_express_template_detail` 表新增两个字段(存储方案选型见下方说明):
> **存储方案选型:推荐逗号分隔单字段,不新建关联表。**
> 理由:① 本业务查询模式是「模板→明细」,非「地区→模板」,无需按地区反查;② 排除地区通常仅几个省,VARCHAR(2000) 足够;③ 改动最小、前端直接透传编码字符串、可快速上线满足客户急迫配置需求。若未来需「按地区统计运费模板」等报表,再迁关联表。
```sql
ALTER TABLE shop_express_template_detail
ADD COLUMN region_mode TINYINT NOT NULL DEFAULT 0 COMMENT '配送区域模式: 0=全国, 1=指定配送(白名单), 2=指定不发货(黑名单)',
ADD COLUMN region_ids VARCHAR(2000) DEFAULT NULL COMMENT '地区编码列表(与regions-data.json一致),逗号分隔,如 110000,120000。region_mode=1或2时有效';
```
- `region_mode=0`(全国):`region_ids` 为空,兼容旧"不选=全国"行为。
- `region_mode=1/2``region_ids` 存选中的地区编码(省/市/区 6 位编码,与前端 `regions-data.json``value` 一致)。
### 旧数据兼容
历史数据 `province_id` / `city_id` 保留不删。迁移建议:
- 旧数据 `region_mode` 默认置 `1``region_ids` = `province_id[,city_id]`(有 city 才拼)。
- 前端编辑时若检测到旧字段(无 `region_mode`),按 `province_id/city_id` 回显到白名单模式。
## 四、region_ids 存储与匹配规则
- 存**最小选中粒度**编码:用户选到省只存省码,选到市存市码(省码可推导,不必冗余存)。
- 编码前缀关系:区码 `110101` ⊃ 市码 `110100` ⊃ 省码 `110000`
- 匹配算法(下单时使用):
- 收货地址解析出 省码 / 市码 / 区码。
- 命中判断:`region_ids` 中包含收货地的 省码 **或** 市码 **或** 区码 即视为命中该条明细。
- 推荐:前端提交时统一存**省+市两级**(不存区),简化逻辑;如需区县级排除再加。
## 五、下单配送校验逻辑(后端 + 小程序端双重校验)
```
收货地址 → 解析 {provinceCode, cityCode, districtCode}
1. 查该商品运费模板下所有 detail 明细
2. 遍历所有 region_mode=2(黑名单) 明细:
若 收货地省/市/区码 任一 ∈ 该明细.region_ids
→ 直接判定【不配送】,返回 "该地区暂不配送"
3. 若无黑名单命中,按现有逻辑选运费明细:
- region_mode=0(全国) 明细 → 匹配
- region_mode=1(白名单) 明细 → 收货地码 ∈ region_ids 才匹配
4. 无任何明细匹配 → 不配送(或走默认模板,按业务定)
```
> 优先级:**黑名单排除 > 白名单/全国匹配**。即一旦落入不发货列表,直接不可下单。
## 六、接口变更
涉及接口(mp-java `ShopExpressTemplateDetailController`):
| 接口 | METHOD & PATH | 变更 |
|------|---------------|------|
| 新增明细 | POST `/shop/shop-express-template-detail` | 入参加 `regionMode``regionIds` |
| 修改明细 | PUT `/shop/shop-express-template-detail` | 入参加 `regionMode``regionIds` |
| 明细详情 | GET `/shop/shop-express-template-detail/{id}` | 出参加 `regionMode``regionIds` |
| 明细列表 | GET `/shop/shop-express-template-detail/page` | 出参加 `regionMode``regionIds` |
入参示例(新增):
```json
{
"templateId": 12,
"type": true,
"regionMode": 2,
"regionIds": "650000,540000,810000,820000,710000",
"firstNum": "1",
"firstAmount": "0",
"extraNum": "1",
"extraAmount": "0",
"status": 0,
"sortNumber": 100
}
```
## 七、需后端确认的问题 —— 已确认(2026-08-06 后端已实现)
> 落地仓库:**guilixu-java**(不是 mp-java;两边 shop 模块原本字节一致,本次只改 guilixu-java)。
| # | 问题 | 结论 |
|---|------|------|
| 1 | 编码 vs 名称 | **存编码**。6 位行政区划码,与 `regions-data.json``value` 一致。 |
| 2 | 粒度 | **省/市/区三级都支持**。前端建议只提交省+市两级;后端匹配时省/市/区任一命中即算命中。 |
| 3 | 黑白名单优先级 | **确认:黑名单命中即不可下单**,优先级高于白名单/全国。允许同一模板下多种模式共存。 |
| 4 | 旧数据迁移 | **由后端 SQL 一次性迁移**,见 `sql/shop_express_template_detail_region.sql`。经核查现网 `province_id/city_id` 存的就是 6 位区划码(如 150000/150200),可直接拼接,无需映射表。 |
| 5 | 下单校验落点 | **后端统一做**,已接入 `OrderBusinessService.createOrder()` 第 3.2 步,防绕过。小程序端可另做前置提示。 |
| 6 | 多模板 | **按租户维度跨模板取并集**。因 `shop_goods` 目前未与运费模板建立关联字段,按"本店除偏远地区外全国发货"的语义处理;后续商品绑定模板后可加 `templateId` 过滤。 |
### 后端实现要点(前端需知)
- **新增字段**`regionMode`(Integer) + `regionIds`(String),已贯通 page / list / get / save / update 全部接口,直接传即可。
- **`regionIds` 格式**:逗号分隔的 6 位编码字符串,如 `"650000,540000,810000,820000,710000"`
- **`regionMode` 取值**`0`=全国、`1`=指定配送(白名单)、`2`=指定不发货(黑名单)。**默认 0**。
- **列表筛选**`regionMode` 支持精确筛选;`regionIds` 支持模糊包含筛选。
- **旧数据回显**:迁移 SQL 已把历史行刷成 `regionMode=1` + `regionIds`,前端**无需**再兼容 `province_id/city_id` 回显逻辑。
- **地址名称→编码**:后端 `RegionCodeResolver` 负责把 `shop_user_address` 里的省/市/区**名称**翻译成编码(该表只存名称不存编码)。已处理 `新疆维吾尔自治区``新疆``北京市``北京` 等后缀归一化。
- **拦截文案**:命中时后端抛 `BusinessException`,接口返回形如 `"新疆维吾尔自治区暂不配送,请更换收货地址"`,前端直接展示 message 即可。
- **放行兜底**:地址解析不出行政区划编码时(境外地址等)**放行不拦截**,避免误伤。
> ⚠️ 上线顺序:必须**先执行** `sql/shop_express_template_detail_region.sql`,再发布后端,否则查询会报未知列。
---
*前端侧对应改造:新建 `RegionsTransfer` 双栏选择组件 + 改造 `shopExpressTemplateDetailEdit.vue` 增加模式切换 + 列表回显。*
+22
View File
@@ -0,0 +1,22 @@
-- ============================================================
-- 修复历史已支付订单的订单商品支付状态(专区销量统计数据补偿)
-- 适用后端:guilixu-java(本部署商城实际后端,db_guilixu)
-- 租户:10606
-- 背景:下单时 shop_order_goods.pay_status 默认 0,支付成功链路此前只更新
-- 主订单 shop_order.pay_status,不会同步订单商品表。专区销量统计按
-- shop_order_goods.pay_status=1 聚合,导致历史已支付订单从未被统计。
-- 作用:把主订单已支付(pay_status=1)且订单商品仍为未支付(pay_status=0)的记录,
-- 同步为已支付,并把 order_status 对齐主订单(退款订单 order_status=6 会被统计口径排除)。
-- 执行前请先备份,并在低峰期执行;可先 SELECT 预览影响行数:
-- SELECT COUNT(*) FROM shop_order_goods og
-- JOIN shop_order o ON og.order_id = o.order_id AND o.tenant_id = 10606
-- WHERE og.tenant_id = 10606 AND o.pay_status = 1 AND og.pay_status = 0;
-- ============================================================
UPDATE shop_order_goods og
JOIN shop_order o ON og.order_id = o.order_id AND o.tenant_id = 10606
SET og.pay_status = 1,
og.order_status = o.order_status
WHERE og.tenant_id = 10606
AND o.pay_status = 1
AND og.pay_status = 0;
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>企业商城</title>
<title>小程序商城</title>
<!-- TinyMCE 通过本地文件加载,避免 Rolldown 打包 UMD 模块的问题 -->
<script src="/tinymce/tinymce.min.js"></script>
<style>
+37
View File
@@ -0,0 +1,37 @@
-- ============================================================
-- 下架「分类ID = 10332 且 没有图片」的商品
-- ------------------------------------------------------------
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
-- 1. 下架 = status = 1(项目约定:0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio
-- 2. 「没有图片」以封面图 image 是否为空判断(封面图为必填项)
-- 3. 已删除商品(deleted = 1)不参与,避免误改
-- 4. 表名/字段名按 snake_case 约定(Node + TypeORM 常见);
-- 若你的库列名是驼峰(goodsId / categoryId),请自行替换
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
-- ============================================================
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE)
SELECT goods_id,
name,
image,
status
FROM shop_goods
WHERE category_id = 10332
AND (image IS NULL OR image = '')
AND (deleted = 0 OR deleted IS NULL);
-- 第二步:确认无误后执行下架
UPDATE shop_goods
SET status = 1,
update_time = NOW()
WHERE category_id = 10332
AND (image IS NULL OR image = '')
AND (deleted = 0 OR deleted IS NULL);
-- ------------------------------------------------------------
-- (可选)若你认为「没有图片」应同时要求轮播图也为空,
-- 即封面图和轮播图都没有才算没图,使用下面更严格的条件:
-- AND (image IS NULL OR image = '')
-- AND (files IS NULL OR files = '' OR files = '[]')
-- ------------------------------------------------------------
+32
View File
@@ -0,0 +1,32 @@
-- ============================================================
-- 偏远地区不发货黑名单(租户 10606,全店生效)
-- ============================================================
-- 作用:下单时 guilixu-java 的 DeliveryRegionChecker 按 tenant_id=10606
-- 取 region_mode=2 且 status=0 的明细,将收货地址行政区划码与
-- region_ids 比对,命中即拦截并提示「XX暂不配送,请更换收货地址」。
-- 注意:黑名单是租户级、全店生效,不区分商品绑定的运费模板。
-- 自提/无需发货(deliveryType=1) 不参与此校验。
-- 省份码(与 regions-data.json 完全一致):
-- 新疆650000 西藏540000 青海630000 宁夏640000 内蒙古150000
-- 海南460000 黑龙江230000 甘肃620000
-- ============================================================
-- 1) 先确认是否已有黑名单(避免重复插入)
SELECT id, template_id, region_mode, region_ids, status, tenant_id
FROM shop_express_template_detail
WHERE tenant_id = 10606 AND region_mode = 2 AND status = 0;
-- 2) 若上一步无记录,再执行插入(template_id=11 即「偏远地区不发」模板)
INSERT INTO shop_express_template_detail
(template_id, type, province_id, city_id, region_mode, region_ids,
first_num, first_amount, extra_amount, extra_num, status, deleted,
tenant_id, create_time, update_time, sort_number)
VALUES
(11, 0, NULL, NULL, 2,
'650000,540000,630000,640000,150000,460000,230000,620000',
0, 0, 0, 0, 0, 0, 10606, NOW(), NOW(), 0);
-- 3) 验证(应能看到刚插入的这条,且 region_ids 非空)
SELECT id, template_id, region_mode, region_ids, status, tenant_id
FROM shop_express_template_detail
WHERE tenant_id = 10606 AND region_mode = 2 AND status = 0;
+52
View File
@@ -0,0 +1,52 @@
-- ============================================================
-- 报表统计菜单(tenant_id=10606,挂在「商城」顶级菜单下)
-- 作用:让前端动态路由 /statistics 自动生效(component=shop/statistics
-- 说明:sys_menu 在 gxwebsoft_core 库;本 SQL 用全限定名,可在任意有权限的库执行。
-- ============================================================
-- 1) 插入报表统计菜单
-- parent_id / app_id 自动取自「商城」顶级菜单(title 含"商城"parent_id=0
-- NOT EXISTS 防止重复插入(重复执行安全)
INSERT INTO gxwebsoft_core.sys_menu
(parent_id, title, path, component, menu_type, sort_number, authority, icon, hide, tenant_id, app_id, deleted)
SELECT
p.menu_id, -- 商城父级 menu_id
'报表统计',
'/statistics',
'shop/statistics',
0, -- 0=菜单
60, -- 排序号(放商城菜单末尾)
'shop:statistics:view',
'BarChartOutlined',
0, -- 0=显示
10606,
p.app_id,
0
FROM gxwebsoft_core.sys_menu p
WHERE p.tenant_id = 10606
AND p.parent_id = 0
AND p.title LIKE '%商城%'
AND NOT EXISTS (
SELECT 1 FROM gxwebsoft_core.sys_menu s
WHERE s.tenant_id = 10606 AND s.component = 'shop/statistics'
)
LIMIT 1;
-- 2) 把新菜单分配给「商城」父级已有的角色(否则角色没有该菜单权限,左侧导航看不到)
INSERT INTO gxwebsoft_core.sys_role_menu (role_id, menu_id)
SELECT rm.role_id, m.menu_id
FROM gxwebsoft_core.sys_menu m
JOIN gxwebsoft_core.sys_menu p ON p.menu_id = m.parent_id
JOIN gxwebsoft_core.sys_role_menu rm ON rm.menu_id = p.menu_id
WHERE m.tenant_id = 10606
AND m.component = 'shop/statistics'
AND NOT EXISTS (
SELECT 1 FROM gxwebsoft_core.sys_role_menu x
WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id
);
-- ============================================================
-- 排查辅助:若第1步未插入(商城父级标题不含"商城"),先用下面语句确认父级 menu_id
-- SELECT menu_id, title, app_id FROM gxwebsoft_core.sys_menu
-- WHERE tenant_id=10606 AND parent_id=0 AND title LIKE '%商城%';
-- ============================================================
+38
View File
@@ -0,0 +1,38 @@
-- ============================================================
-- 批量上架「分类ID = 10332 且 含有图片」的商品
-- ------------------------------------------------------------
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
-- 1. 上架 = status = 0(项目约定:0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio
-- 2. 「含有图片」以封面图 image 是否非空判断(image IS NOT NULL AND image <> ''
-- 3. 已删除商品(deleted = 1)不参与,避免误改
-- 4. 表名/字段名按 snake_case 约定(Node + TypeORM 常见);
-- 若你的库列名是驼峰(goodsId / categoryId),请自行替换
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
-- ============================================================
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE)
SELECT goods_id,
name,
image,
status
FROM shop_goods
WHERE category_id = 10332
AND image IS NOT NULL
AND image <> ''
AND (deleted = 0 OR deleted IS NULL);
-- 第二步:确认无误后执行上架
UPDATE shop_goods
SET status = 0,
update_time = NOW()
WHERE category_id = 10332
AND image IS NOT NULL
AND image <> ''
AND (deleted = 0 OR deleted IS NULL);
-- ------------------------------------------------------------
-- (可选)若只想把「当前已下架」的商品重新上架,
-- 而不动「待审核(2) / 审核不通过(3)」的商品,加上 status 条件:
-- AND status = 1
-- ------------------------------------------------------------
+4
View File
@@ -32,6 +32,8 @@
"@bytemd/plugin-gfm": "^1.17.2",
"@bytemd/plugin-highlight": "^1.17.4",
"@bytemd/plugin-highlight-ssr": "^1.20.2",
"@wangeditor/editor": "^5",
"@wangeditor/editor-for-vue": "^5",
"@wecom/jssdk": "^1.3.1",
"ali-oss": "^6.18.0",
"ant-design-vue": "^4.2.6",
@@ -53,6 +55,7 @@
"js-md5": "^0.7.3",
"jsbarcode": "^3.11.5",
"lodash-es": "^4.17.21",
"markdown-it": "^14",
"md-editor-v3": "^5.2.3",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
@@ -73,6 +76,7 @@
"@types/dompurify": "^3.2.0",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.6",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.15.0",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.5",
+395
View File
@@ -34,6 +34,12 @@ importers:
'@bytemd/plugin-highlight-ssr':
specifier: ^1.20.2
version: 1.22.0(bytemd@1.22.0)
'@wangeditor/editor':
specifier: ^5
version: 5.1.23
'@wangeditor/editor-for-vue':
specifier: ^5
version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.3(typescript@5.9.3))
'@wecom/jssdk':
specifier: ^1.3.1
version: 1.4.5
@@ -97,6 +103,9 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
markdown-it:
specifier: ^14
version: 14.1.0
md-editor-v3:
specifier: ^5.2.3
version: 5.8.5(vue@3.5.3(typescript@5.9.3))
@@ -152,6 +161,9 @@ importers:
'@types/lodash-es':
specifier: ^4.17.6
version: 4.17.12
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
'@types/node':
specifier: ^22.15.0
version: 22.19.18
@@ -1371,6 +1383,9 @@ packages:
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@transloadit/prettier-bytes@0.0.7':
resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==}
'@tybys/wasm-util@0.10.2':
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
@@ -1444,6 +1459,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/event-emitter@0.3.5':
resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==}
'@types/file-saver@2.0.7':
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
@@ -1566,6 +1584,23 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@uppy/companion-client@2.2.2':
resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==}
'@uppy/core@2.3.4':
resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==}
'@uppy/store-default@2.1.1':
resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==}
'@uppy/utils@4.1.3':
resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==}
'@uppy/xhr-upload@2.1.3':
resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==}
peerDependencies:
'@uppy/core': ^2.3.3
'@vavt/copy2clipboard@1.0.3':
resolution: {integrity: sha512-HtG48r2FBYp9eRvGB3QGmtRBH1zzRRAVvFbGgFstOwz4/DDaNiX0uZc3YVKPydqgOav26pibr9MtoCaWxn7aeA==}
@@ -1654,6 +1689,93 @@ packages:
'@vue/shared@3.5.34':
resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==}
'@wangeditor/basic-modules@1.1.7':
resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.throttle: ^4.1.1
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/code-highlight@1.0.3':
resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/core@1.1.19':
resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==}
peerDependencies:
'@uppy/core': ^2.1.1
'@uppy/xhr-upload': ^2.0.3
dom7: ^3.0.0
is-hotkey: ^0.2.0
lodash.camelcase: ^4.3.0
lodash.clonedeep: ^4.5.0
lodash.debounce: ^4.0.8
lodash.foreach: ^4.5.0
lodash.isequal: ^4.5.0
lodash.throttle: ^4.1.1
lodash.toarray: ^4.4.0
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/editor-for-vue@5.1.12':
resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==}
peerDependencies:
'@wangeditor/editor': '>=5.1.0'
vue: ^3.0.5
'@wangeditor/editor@5.1.23':
resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==}
'@wangeditor/list-module@1.0.5':
resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/table-module@1.1.4':
resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.isequal: ^4.5.0
lodash.throttle: ^4.1.1
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/upload-image-module@1.0.2':
resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==}
peerDependencies:
'@uppy/core': ^2.0.3
'@uppy/xhr-upload': ^2.0.3
'@wangeditor/basic-modules': 1.x
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.foreach: ^4.5.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/video-module@1.1.4':
resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==}
peerDependencies:
'@uppy/core': ^2.1.4
'@uppy/xhr-upload': ^2.0.7
'@wangeditor/core': 1.x
dom7: ^3.0.0
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wecom/jssdk@1.4.5':
resolution: {integrity: sha512-1SSmcDUid7QOQNNFaM4d884NXJQYuoO+oLZtM9TRO5uMXYj2s69hmuAoPUr3D3br6VDgv4H+tbIG5A5A+uB9lA==}
@@ -2256,6 +2378,9 @@ packages:
dom-scroll-into-view@2.0.1:
resolution: {integrity: sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==}
dom7@3.0.0:
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
dompurify@3.4.5:
resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==}
@@ -2728,6 +2853,9 @@ packages:
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
i18next@20.6.1:
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -2751,6 +2879,9 @@ packages:
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -2809,6 +2940,9 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-hotkey@0.2.0:
resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
@@ -2825,9 +2959,16 @@ packages:
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
engines: {node: '>=0.10.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-type-of@1.4.0:
resolution: {integrity: sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==}
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
is-what@3.14.1:
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
@@ -3024,6 +3165,12 @@ packages:
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
lodash.clonedeep@4.5.0:
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
@@ -3039,6 +3186,9 @@ packages:
lodash.flatten@4.4.0:
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
lodash.foreach@4.5.0:
resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
lodash.groupby@4.6.0:
resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==}
@@ -3064,6 +3214,12 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
lodash.toarray@4.4.0:
resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==}
lodash.union@4.6.0:
resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==}
@@ -3278,6 +3434,9 @@ packages:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-match@1.0.2:
resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
@@ -3332,6 +3491,9 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
namespace-emitter@2.0.1:
resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -3584,6 +3746,14 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
preact@10.29.8:
resolution: {integrity: sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==}
peerDependencies:
preact-render-to-string: '>=5'
peerDependenciesMeta:
preact-render-to-string:
optional: true
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -3597,6 +3767,10 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -3841,6 +4015,18 @@ packages:
simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
slate-history@0.66.0:
resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==}
peerDependencies:
slate: '>=0.65.3'
slate@0.72.8:
resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==}
snabbdom@3.6.4:
resolution: {integrity: sha512-VmxEfuw1/Y/eFj5VtMhYnukExpYiPkNzoo3+N3qwAOUDMl8wXgbli5ebR+j0knE3lZ/0eYskLxNcX64uy10N9w==}
engines: {node: '>=12.17.0'}
socket.io-client@4.8.1:
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
engines: {node: '>=10.0.0'}
@@ -3881,6 +4067,9 @@ packages:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
ssr-window@3.0.0:
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
statuses@1.5.0:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
@@ -3973,6 +4162,9 @@ packages:
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@@ -4311,6 +4503,9 @@ packages:
engines: {node: '>= 8'}
hasBin: true
wildcard@1.1.2:
resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
win-release@1.1.1:
resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==}
engines: {node: '>=0.10.0'}
@@ -5993,6 +6188,8 @@ snapshots:
'@socket.io/component-emitter@3.1.2': {}
'@transloadit/prettier-bytes@0.0.7': {}
'@tybys/wasm-util@0.10.2':
dependencies:
tslib: 2.8.1
@@ -6060,6 +6257,8 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/event-emitter@0.3.5': {}
'@types/file-saver@2.0.7': {}
'@types/geojson@7946.0.16': {}
@@ -6211,6 +6410,37 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@uppy/companion-client@2.2.2':
dependencies:
'@uppy/utils': 4.1.3
namespace-emitter: 2.0.1
'@uppy/core@2.3.4':
dependencies:
'@transloadit/prettier-bytes': 0.0.7
'@uppy/store-default': 2.1.1
'@uppy/utils': 4.1.3
lodash.throttle: 4.1.1
mime-match: 1.0.2
namespace-emitter: 2.0.1
nanoid: 3.3.11
preact: 10.29.8
transitivePeerDependencies:
- preact-render-to-string
'@uppy/store-default@2.1.1': {}
'@uppy/utils@4.1.3':
dependencies:
lodash.throttle: 4.1.1
'@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)':
dependencies:
'@uppy/companion-client': 2.2.2
'@uppy/core': 2.3.4
'@uppy/utils': 4.1.3
nanoid: 3.3.11
'@vavt/copy2clipboard@1.0.3': {}
'@vavt/util@2.1.1': {}
@@ -6365,6 +6595,116 @@ snapshots:
'@vue/shared@3.5.34': {}
'@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
is-url: 1.2.4
lodash.throttle: 4.1.1
nanoid: 3.3.11
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
prismjs: 1.30.0
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@types/event-emitter': 0.3.5
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
dom7: 3.0.0
event-emitter: 0.3.5
html-void-elements: 2.0.1
i18next: 20.6.1
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
lodash.clonedeep: 4.5.0
lodash.debounce: 4.0.8
lodash.foreach: 4.5.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 3.3.11
scroll-into-view-if-needed: 2.2.31
slate: 0.72.8
slate-history: 0.66.0(slate@0.72.8)
snabbdom: 3.6.4
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.3(typescript@5.9.3))':
dependencies:
'@wangeditor/editor': 5.1.23
vue: 3.5.3(typescript@5.9.3)
'@wangeditor/editor@5.1.23':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
lodash.clonedeep: 4.5.0
lodash.debounce: 4.0.8
lodash.foreach: 4.5.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 3.3.11
slate: 0.72.8
snabbdom: 3.6.4
transitivePeerDependencies:
- preact-render-to-string
'@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
nanoid: 3.3.11
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
lodash.foreach: 4.5.0
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
nanoid: 3.3.11
slate: 0.72.8
snabbdom: 3.6.4
'@wecom/jssdk@1.4.5': {}
acorn-jsx@5.3.2(acorn@8.15.0):
@@ -7006,6 +7346,10 @@ snapshots:
dom-scroll-into-view@2.0.1: {}
dom7@3.0.0:
dependencies:
ssr-window: 3.0.0
dompurify@3.4.5:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -7556,6 +7900,10 @@ snapshots:
dependencies:
ms: 2.1.3
i18next@20.6.1:
dependencies:
'@babel/runtime': 7.28.4
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -7571,6 +7919,8 @@ snapshots:
immediate@3.0.6: {}
immer@9.0.21: {}
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -7613,6 +7963,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-hotkey@0.2.0: {}
is-number@7.0.0: {}
is-path-inside@3.0.3: {}
@@ -7621,12 +7973,16 @@ snapshots:
is-plain-object@3.0.1: {}
is-plain-object@5.0.0: {}
is-type-of@1.4.0:
dependencies:
core-util-is: 1.0.3
is-class-hotfix: 0.0.6
isstream: 0.1.2
is-url@1.2.4: {}
is-what@3.14.1: {}
is-wsl@2.2.0:
@@ -7792,6 +8148,10 @@ snapshots:
lodash-es@4.17.21: {}
lodash.camelcase@4.3.0: {}
lodash.clonedeep@4.5.0: {}
lodash.debounce@4.0.8: {}
lodash.defaults@4.2.0: {}
@@ -7802,6 +8162,8 @@ snapshots:
lodash.flatten@4.4.0: {}
lodash.foreach@4.5.0: {}
lodash.groupby@4.6.0: {}
lodash.isboolean@3.0.3: {}
@@ -7818,6 +8180,10 @@ snapshots:
lodash.merge@4.6.2: {}
lodash.throttle@4.1.1: {}
lodash.toarray@4.4.0: {}
lodash.union@4.6.0: {}
lodash.uniq@4.5.0: {}
@@ -8215,6 +8581,10 @@ snapshots:
mime-db@1.52.0: {}
mime-match@1.0.2:
dependencies:
wildcard: 1.1.2
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
@@ -8265,6 +8635,8 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
namespace-emitter@2.0.1: {}
nanoid@3.3.11: {}
nanoid@5.1.6: {}
@@ -8469,6 +8841,8 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
preact@10.29.8: {}
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
@@ -8477,6 +8851,8 @@ snapshots:
prettier@2.8.8: {}
prismjs@1.30.0: {}
process-nextick-args@2.0.1: {}
property-information@6.5.0: {}
@@ -8784,6 +9160,19 @@ snapshots:
dependencies:
is-arrayish: 0.3.4
slate-history@0.66.0(slate@0.72.8):
dependencies:
is-plain-object: 5.0.0
slate: 0.72.8
slate@0.72.8:
dependencies:
immer: 9.0.21
is-plain-object: 5.0.0
tiny-warning: 1.0.3
snabbdom@3.6.4: {}
socket.io-client@4.8.1:
dependencies:
'@socket.io/component-emitter': 3.1.2
@@ -8827,6 +9216,8 @@ snapshots:
dependencies:
frac: 1.1.2
ssr-window@3.0.0: {}
statuses@1.5.0: {}
stream-http@2.8.2:
@@ -8949,6 +9340,8 @@ snapshots:
through@2.3.8: {}
tiny-warning@1.0.3: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
@@ -9284,6 +9677,8 @@ snapshots:
dependencies:
isexe: 2.0.0
wildcard@1.1.2: {}
win-release@1.1.1:
dependencies:
semver: 5.7.2
+43
View File
@@ -0,0 +1,43 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { AppProduct, AppProductQueryParam } from './model';
/**
* websopy 特殊接口域名
* 后端 Controller: com.gxwebsoft.app.controller.AppProductController
* @RequestMapping("/api/app/product"),后端无 context-path
* 完整 URL = https://websopy-api.websoft.top/api/app/product/xxx
*/
const WEBSOPY_API_BASE = 'https://websopy-api.websoft.top/api';
const BASE = `${WEBSOPY_API_BASE}/app/product`;
/**
* 分页查询应用列表
* GET /app/product/page
* 支持按 tenantId 过滤(管理后台按租户查询应用)
* 注意:分页参数为 current/size(非 page/limit
*/
export async function pageProducts(
params: AppProductQueryParam
): Promise<PageResult<AppProduct>> {
const res = await request.get<ApiResult<PageResult<AppProduct>>>(
`${BASE}/page`,
{ params }
);
if (res.data.code === 0) {
return res.data.data || { list: [], count: 0 };
}
return Promise.reject(new Error(res.data.message));
}
/**
* 获取应用详情
* GET /app/product/detail/{id}
*/
export async function getProductDetail(id: number): Promise<AppProduct> {
const res = await request.get<ApiResult<AppProduct>>(`${BASE}/detail/${id}`);
if (res.data.code === 0) {
return res.data.data as AppProduct;
}
return Promise.reject(new Error(res.data.message));
}
+143
View File
@@ -0,0 +1,143 @@
/**
* 应用产品实体(对应表 app_product
* 字段对照后端 com.gxwebsoft.app.entity.AppProduct
*/
export interface AppProduct {
// 应用ID(主键)
productId?: number;
// 应用名称
productName?: string;
// 应用标识(唯一)
productCode?: string;
// 应用密钥
productSecret?: string;
// 应用类型: 10网站 20微信小程序 30抖音小程序 40百度小程序 50支付宝小程序 60Android 70iOS 80macOS 90Windows 100插件
appType?: number;
// 应用类型名称(关联查询)
appTypeName?: string;
// 分类ID
categoryId?: number;
// 行业类型(父级)
industryParent?: string;
// 行业类型(子级)
industryChild?: string;
// 应用Logo
logo?: string;
// 应用图标
icon?: string;
// 二维码
qrcode?: string;
// 应用截图(JSON数组)
screenshots?: string;
// 应用简介
description?: string;
// 详细说明
content?: string;
// 关键词
keywords?: string;
// 域名
domain?: string;
// 域名前缀
prefix?: string;
// 包名/AppID
packageName?: string;
// 后台地址
adminUrl?: string;
// API地址
apiUrl?: string;
// 下载地址
downloadUrl?: string;
// 版本号
version?: string;
// 版本: standard标准版 professional专业版 perpetual永久授权
edition?: string;
// 最低版本要求
minVersion?: string;
// 定价: free免费 one_time一次性 subscription订阅
priceType?: string;
// 价格(元)
price?: number;
// 划线价格
linePrice?: number;
// 续费价格
renewPrice?: number;
// 交付方式: 1源码 2托管 3授权
deliveryMethod?: number;
// 计费方式: 1按年 2按月 3一次性
chargingMethod?: number;
// 订阅周期: month/year
subscriptionPeriod?: string;
// 发布状态: developing pending_review published rejected deprecated
publishStatus?: string;
// 发布时间
publishTime?: string;
// 审核时间
reviewTime?: string;
// 审核人ID
reviewerId?: number;
// 拒绝原因
rejectReason?: string;
// 浏览次数
clicks?: number;
// 安装次数
installs?: number;
// 下载次数
downloads?: number;
// 评分(1-5)
rating?: number;
// 点赞数
likes?: number;
// 开发者
developer?: string;
// 开发者电话
developerPhone?: string;
// 开发者邮箱
developerEmail?: string;
// 是否推荐: 0否 1是
recommend?: number;
// 是否官方: 0否 1是
official?: number;
// 是否上架市场: 0否 1是
market?: number;
// 是否显示首页: 0否 1是
showIndex?: number;
// 是否可搜索: 0否 1是
searchEnabled?: number;
// 模板ID
templateId?: number;
// 租户ID
tenantId?: number;
// 创建时间
createTime?: string;
// 更新时间
updateTime?: string;
}
/**
* 应用产品分页查询参数
* 注意:后端 AppProductController.page 的分页参数为 current/size(非 page/limit
*/
export interface AppProductQueryParam {
// 页码
current?: number;
// 每页条数
size?: number;
// 应用名称
productName?: string;
// 应用标识
productCode?: string;
// 应用类型
appType?: number;
// 分类ID
categoryId?: number;
// 发布状态
publishStatus?: string;
// 状态
status?: number;
// 用户ID
userId?: number;
// 租户ID(按租户查询应用)
tenantId?: number;
// 关键词搜索(同时搜索应用名称和应用标识)
keywords?: string;
}
+276
View File
@@ -0,0 +1,276 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type {
AppSubscription,
AppSubscriptionQueryParam,
SubscribeParam,
SubscribeResult,
GeneratePayQrcodeParam,
GeneratePayQrcodeResult,
CheckStatusResult,
PayResult,
WechatNativePayResult,
MpPrepayParam,
MpPrepayResult,
MpConfirmParam,
BalanceResult
} from './model';
/**
* websopy 特殊接口域名(与项目主 API 域名不同)
* 后端 Controller: com.gxwebsoft.app.controller.AppSubscriptionController
* @RequestMapping("/api/app/subscription"),后端无 context-path
* 因此完整 URL = https://websopy-api.websoft.top + /api/app/subscription/xxx
*/
const WEBSOPY_API_BASE = 'https://websopy-api.websoft.top/api';
const BASE = `${WEBSOPY_API_BASE}/app/subscription`;
/**
* 我的订阅列表(分页)
* GET /app/subscription/my/page
*/
export async function pageMySubscriptions(
params: AppSubscriptionQueryParam
): Promise<PageResult<AppSubscription>> {
const res = await request.get<ApiResult<PageResult<AppSubscription>>>(
`${BASE}/my/page`,
{ params }
);
if (res.data.code === 0) {
return (
res.data.data || { list: [], count: 0 }
);
}
return Promise.reject(new Error(res.data.message));
}
/**
* 订阅详情(按ID
* GET /app/subscription/detail/{id}
*/
export async function getSubscriptionDetail(
id: number
): Promise<AppSubscription> {
const res = await request.get<ApiResult<AppSubscription>>(
`${BASE}/detail/${id}`
);
if (res.data.code === 0) {
return res.data.data as AppSubscription;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据订阅编号查询详情(小程序入口用)
* GET /app/subscription/detail-by-no/{subscriptionNo}
*/
export async function getSubscriptionDetailByNo(
subscriptionNo: string
): Promise<AppSubscription> {
const res = await request.get<ApiResult<AppSubscription>>(
`${BASE}/detail-by-no/${subscriptionNo}`
);
if (res.data.code === 0) {
return res.data.data as AppSubscription;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询支付状态(前端轮询用)
* GET /app/subscription/check-status/{subscriptionNo}
*/
export async function checkSubscriptionStatus(
subscriptionNo: string
): Promise<CheckStatusResult> {
const res = await request.get<ApiResult<CheckStatusResult>>(
`${BASE}/check-status/${subscriptionNo}`
);
if (res.data.code === 0) {
return res.data.data as CheckStatusResult;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 检查是否已购买某应用
* GET /app/subscription/check-purchased/{productId}
* 返回 boolean
*/
export async function checkPurchased(
productId: number
): Promise<boolean> {
const res = await request.get<ApiResult<boolean>>(
`${BASE}/check-purchased/${productId}`
);
if (res.data.code === 0) {
return res.data.data === true;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 获取当前用户余额
* GET /app/subscription/balance
*/
export async function getBalance(): Promise<BalanceResult> {
const res = await request.get<ApiResult<BalanceResult>>(`${BASE}/balance`);
if (res.data.code === 0) {
return (
res.data.data || { balance: 0 }
);
}
return Promise.reject(new Error(res.data.message));
}
/**
* 创建订阅
* POST /app/subscription/subscribe
* 免费应用直接激活,付费应用创建待支付记录
*/
export async function subscribe(
data: SubscribeParam
): Promise<SubscribeResult> {
const res = await request.post<ApiResult<SubscribeResult>>(
`${BASE}/subscribe`,
data
);
if (res.data.code === 0) {
return res.data.data as SubscribeResult;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 生成支付小程序码(同时创建订阅记录)
* POST /app/subscription/generate-pay-qrcode
*/
export async function generatePayQrcode(
data: GeneratePayQrcodeParam
): Promise<GeneratePayQrcodeResult> {
const res = await request.post<ApiResult<GeneratePayQrcodeResult>>(
`${BASE}/generate-pay-qrcode`,
data
);
if (res.data.code === 0) {
return res.data.data as GeneratePayQrcodeResult;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 发起支付
* POST /app/subscription/pay/{id}?method=balance|wechat&envVersion=xxx
* - method=balance:余额支付,返回 PayResult
* - method=wechat:微信 Native 支付,返回小程序码 WechatNativePayResult
*/
export async function paySubscription(
id: number,
method: 'balance' | 'wechat' = 'wechat',
envVersion?: string
): Promise<PayResult | WechatNativePayResult> {
const res = await request.post<ApiResult<PayResult | WechatNativePayResult>>(
`${BASE}/pay/${id}`,
undefined,
{ params: { method, envVersion } }
);
if (res.data.code === 0) {
return res.data.data as PayResult | WechatNativePayResult;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 小程序 JSAPI 预支付下单
* POST /app/subscription/mp-prepay/{id}
* Body: { openid }
*/
export async function mpPrepay(
id: number,
data: MpPrepayParam
): Promise<MpPrepayResult> {
const res = await request.post<ApiResult<MpPrepayResult>>(
`${BASE}/mp-prepay/${id}`,
data
);
if (res.data.code === 0) {
return res.data.data as MpPrepayResult;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 小程序支付成功确认
* POST /app/subscription/mp-confirm/{subscriptionNo}
* Body: { transactionId? }
*/
export async function mpConfirm(
subscriptionNo: string,
data?: MpConfirmParam
): Promise<PayResult> {
const res = await request.post<ApiResult<PayResult>>(
`${BASE}/mp-confirm/${subscriptionNo}`,
data
);
if (res.data.code === 0) {
return (
res.data.data || { paid: true, subscriptionNo }
);
}
return Promise.reject(new Error(res.data.message));
}
/**
* 续费:基于已有订阅创建续费订单并生成支付码(一步完成,对齐后端 /renew-pay
* POST /app/subscription/renew-pay/{id}?period=month&method=wechat&envVersion=trial
* - method=balance:余额支付,返回 PayResultpaid/balance
* - method=wechat:微信支付,返回小程序码 WechatNativePayResultminiappQrcode
* 后端会基于原 expireTime 或 now(取较大者)延长到期时间
*/
export async function renewSubscription(
id: number,
period: 'month' | 'year' = 'month',
method: 'balance' | 'wechat' = 'wechat',
envVersion?: string
): Promise<PayResult | WechatNativePayResult> {
const res = await request.post<ApiResult<PayResult | WechatNativePayResult>>(
`${BASE}/renew-pay/${id}`,
undefined,
{ params: { period, method, envVersion } }
);
if (res.data.code === 0) {
return res.data.data as PayResult | WechatNativePayResult;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 退订/取消
* POST /app/subscription/cancel/{id}
*/
export async function cancelSubscription(id: number): Promise<string> {
const res = await request.post<ApiResult<string>>(`${BASE}/cancel/${id}`);
if (res.data.code === 0) {
return res.data.message || '退订成功';
}
return Promise.reject(new Error(res.data.message));
}
/**
* 启用/禁用
* POST /app/subscription/toggle-enable/{id}?enabled=true|false
*/
export async function toggleEnable(
id: number,
enabled: boolean
): Promise<string> {
const res = await request.post<ApiResult<string>>(
`${BASE}/toggle-enable/${id}`,
undefined,
{ params: { enabled } }
);
if (res.data.code === 0) {
return res.data.message || (enabled ? '已启用' : '已禁用');
}
return Promise.reject(new Error(res.data.message));
}
+219
View File
@@ -0,0 +1,219 @@
import type { PageParam } from '@/api';
/**
* 订阅状态: pending-待支付 active-已激活 expired-已过期 cancelled-已取消
*/
export type SubscriptionStatus =
| 'pending'
| 'active'
| 'expired'
| 'cancelled';
/**
* 价格类型: free-免费 one_time-买断 subscription-订阅
*/
export type PriceType = 'free' | 'one_time' | 'subscription';
/**
* 订阅周期: month-月 quarter-季 year-年
*/
export type SubscriptionPeriod = 'month' | 'quarter' | 'year';
/**
* 支付方式: 0-余额 1-微信 2-支付宝 12-免费
*/
export type PayType = 0 | 1 | 2 | 12;
/**
* 应用订阅实体(对应表 app_subscription
* 字段对照后端 com.gxwebsoft.app.entity.AppSubscription
*/
export interface AppSubscription {
// 主键ID
id?: number;
// 订阅编号(业务唯一)
subscriptionNo?: string;
// 购买用户ID
userId?: number;
// 应用产品ID
productId?: number;
// 租户ID
tenantId?: number;
// 订阅状态
status?: SubscriptionStatus;
// 价格类型
priceType?: PriceType;
// 原价(单位:元)
originalPrice?: number;
// 实付金额(单位:元)
payPrice?: number;
// 支付方式
payType?: PayType;
// 支付状态: 0-未支付 1-已支付
payStatus?: number;
// 支付时间
payTime?: string;
// 第三方交易号
transactionId?: string;
// 订阅周期
subscriptionPeriod?: SubscriptionPeriod;
// 生效时间
startTime?: string;
// 到期时间(订阅型)
expireTime?: string;
// 是否自动续费 0-否 1-是
autoRenew?: number;
// 分配的域名
instanceDomain?: string;
// 实例管理后台URL
instanceAdminUrl?: string;
// 实例配置(JSON
instanceConfig?: string;
// 关联的支付订单号
orderNo?: string;
// 关联的支付订单ID
orderId?: number;
// 备注
remark?: string;
// 排序
sortNumber?: number;
// 创建时间
createTime?: string;
// 更新时间
updateTime?: string;
// ===== 关联查询字段(非数据库字段) =====
productName?: string;
productIcon?: string;
productLogo?: string;
productAppType?: number;
productDescription?: string;
developerName?: string;
}
/**
* 我的订阅分页查询参数
*/
export interface AppSubscriptionQueryParam extends PageParam {
// 订阅状态过滤
status?: SubscriptionStatus;
}
/**
* 创建订阅参数
*/
export interface SubscribeParam {
// 应用产品ID
productId: number;
// 订阅周期,默认 month
subscriptionPeriod?: SubscriptionPeriod;
}
/**
* 生成支付小程序码参数
*/
export interface GeneratePayQrcodeParam {
// 应用产品ID
productId: number;
// 订阅周期,默认 month
subscriptionPeriod?: SubscriptionPeriod;
// 小程序版本:develop / trial / release,默认 trial
envVersion?: string;
}
/**
* 创建订阅返回结果
*/
export interface SubscribeResult {
subscriptionId: number;
subscriptionNo: string;
status: SubscriptionStatus;
message: string;
payPrice?: number;
orderNo?: string;
}
/**
* 生成支付小程序码返回结果
*/
export interface GeneratePayQrcodeResult {
subscriptionNo: string;
subscriptionId: number;
qrcodeBase64: string;
productName: string;
payPrice: number;
}
/**
* 查询支付状态返回结果
*/
export interface CheckStatusResult {
paid: boolean;
payStatus: number;
status: SubscriptionStatus;
payTime?: string;
transactionId?: string;
id: number;
subscriptionNo: string;
productId: number;
productName?: string;
productLogo?: string;
priceType?: PriceType;
payPrice?: number;
subscriptionPeriod?: SubscriptionPeriod;
}
/**
* 余额支付返回结果
*/
export interface PayResult {
paid: boolean;
balance?: number;
subscriptionNo: string;
}
/**
* 微信 Native 支付(Web 端)返回结果
*/
export interface WechatNativePayResult {
subscriptionId: number;
subscriptionNo: string;
miniappQrcode: string;
miniappPagePath: string;
payPrice: number;
}
/**
* 小程序 JSAPI 预支付返回结果
*/
export interface MpPrepayResult {
subscriptionId: number;
subscriptionNo: string;
outTradeNo: string;
timeStamp: string;
nonceStr: string;
package: string;
signType: string;
paySign: string;
payPrice: number;
}
/**
* 获取余额返回结果
*/
export interface BalanceResult {
balance: number;
}
/**
* 小程序预支付参数
*/
export interface MpPrepayParam {
openid: string;
}
/**
* 小程序支付确认参数
*/
export interface MpConfirmParam {
transactionId?: string;
}
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsAd, CmsAdParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询广告位
*/
export async function pageCmsAd(params: CmsAdParam) {
const res = await request.get<ApiResult<PageResult<CmsAd>>>(
MODULES_API_URL + '/cms/cms-ad/page',
CMS_API_BASE_URL + '/cms/cms-ad/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsAd(params: CmsAdParam) {
*/
export async function listCmsAd(params?: CmsAdParam) {
const res = await request.get<ApiResult<CmsAd[]>>(
MODULES_API_URL + '/cms/cms-ad',
CMS_API_BASE_URL + '/cms/cms-ad',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsAd(params?: CmsAdParam) {
*/
export async function addCmsAd(data: CmsAd) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-ad',
CMS_API_BASE_URL + '/cms/cms-ad',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsAd(data: CmsAd) {
*/
export async function updateCmsAd(data: CmsAd) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-ad',
CMS_API_BASE_URL + '/cms/cms-ad',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsAd(data: CmsAd) {
*/
export async function removeCmsAd(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-ad/' + id
CMS_API_BASE_URL + '/cms/cms-ad/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsAd(id?: number) {
*/
export async function removeBatchCmsAd(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-ad/batch',
CMS_API_BASE_URL + '/cms/cms-ad/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsAd(data: (number | undefined)[]) {
*/
export async function getCmsAd(id: number) {
const res = await request.get<ApiResult<CmsAd>>(
MODULES_API_URL + '/cms/cms-ad/' + id
CMS_API_BASE_URL + '/cms/cms-ad/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsAdRecord, CmsAdRecordParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询广告图片
*/
export async function pageCmsAdRecord(params: CmsAdRecordParam) {
const res = await request.get<ApiResult<PageResult<CmsAdRecord>>>(
MODULES_API_URL + '/cms/cms-ad-record/page',
CMS_API_BASE_URL + '/cms/cms-ad-record/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsAdRecord(params: CmsAdRecordParam) {
*/
export async function listCmsAdRecord(params?: CmsAdRecordParam) {
const res = await request.get<ApiResult<CmsAdRecord[]>>(
MODULES_API_URL + '/cms/cms-ad-record',
CMS_API_BASE_URL + '/cms/cms-ad-record',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsAdRecord(params?: CmsAdRecordParam) {
*/
export async function addCmsAdRecord(data: CmsAdRecord) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-ad-record',
CMS_API_BASE_URL + '/cms/cms-ad-record',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsAdRecord(data: CmsAdRecord) {
*/
export async function updateCmsAdRecord(data: CmsAdRecord) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-ad-record',
CMS_API_BASE_URL + '/cms/cms-ad-record',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsAdRecord(data: CmsAdRecord) {
*/
export async function removeCmsAdRecord(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-ad-record/' + id
CMS_API_BASE_URL + '/cms/cms-ad-record/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsAdRecord(id?: number) {
*/
export async function removeBatchCmsAdRecord(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-ad-record/batch',
CMS_API_BASE_URL + '/cms/cms-ad-record/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsAdRecord(data: (number | undefined)[]) {
*/
export async function getCmsAdRecord(id: number) {
const res = await request.get<ApiResult<CmsAdRecord>>(
MODULES_API_URL + '/cms/cms-ad-record/' + id
CMS_API_BASE_URL + '/cms/cms-ad-record/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+12 -11
View File
@@ -1,4 +1,5 @@
import request from '@/utils/request';
import { CMS_API_BASE_URL } from '@/config/setting';
import type { ApiResult, PageResult } from '@/api';
import type { CmsArticle, CmsArticleParam } from './model';
@@ -7,7 +8,7 @@ import type { CmsArticle, CmsArticleParam } from './model';
*/
export async function pageCmsArticle(params: CmsArticleParam) {
const res = await request.get<ApiResult<PageResult<CmsArticle>>>(
'/cms/cms-article/page',
CMS_API_BASE_URL + '/cms/cms-article/page',
{
params
}
@@ -22,7 +23,7 @@ export async function pageCmsArticle(params: CmsArticleParam) {
* 查询文章列表
*/
export async function listCmsArticle(params?: CmsArticleParam) {
const res = await request.get<ApiResult<CmsArticle[]>>('/cms/cms-article', {
const res = await request.get<ApiResult<CmsArticle[]>>(CMS_API_BASE_URL + '/cms/cms-article', {
params
});
if (res.data.code === 0 && res.data.data) {
@@ -35,7 +36,7 @@ export async function listCmsArticle(params?: CmsArticleParam) {
* 添加文章
*/
export async function addCmsArticle(data: CmsArticle) {
const res = await request.post<ApiResult<unknown>>('/cms/cms-article', data);
const res = await request.post<ApiResult<unknown>>(CMS_API_BASE_URL + '/cms/cms-article', data);
if (res.data.code === 0) {
return res.data.message;
}
@@ -46,7 +47,7 @@ export async function addCmsArticle(data: CmsArticle) {
* 修改文章
*/
export async function updateCmsArticle(data: CmsArticle) {
const res = await request.put<ApiResult<unknown>>('/cms/cms-article', data);
const res = await request.put<ApiResult<unknown>>(CMS_API_BASE_URL + '/cms/cms-article', data);
if (res.data.code === 0) {
return res.data.message;
}
@@ -58,7 +59,7 @@ export async function updateCmsArticle(data: CmsArticle) {
*/
export async function updateBatchCmsArticle(data: any) {
const res = await request.put<ApiResult<unknown>>(
'/cms/cms-article/batch',
CMS_API_BASE_URL + '/cms/cms-article/batch',
data
);
if (res.data.code === 0) {
@@ -72,7 +73,7 @@ export async function updateBatchCmsArticle(data: any) {
*/
export async function removeCmsArticle(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/cms/cms-article/' + id
CMS_API_BASE_URL + '/cms/cms-article/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -85,7 +86,7 @@ export async function removeCmsArticle(id?: number) {
*/
export async function removeBatchCmsArticle(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/cms/cms-article/batch',
CMS_API_BASE_URL + '/cms/cms-article/batch',
{
data
}
@@ -101,7 +102,7 @@ export async function removeBatchCmsArticle(data: (number | undefined)[]) {
*/
export async function getCmsArticle(id: number) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/' + id
CMS_API_BASE_URL + '/cms/cms-article/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -114,7 +115,7 @@ export async function getCmsArticle(id: number) {
*/
export async function getByCode(code: string) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/getByCode/' + code
CMS_API_BASE_URL + '/cms/cms-article/getByCode/' + code
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -123,7 +124,7 @@ export async function getByCode(code: string) {
}
export async function getCount(params: CmsArticleParam) {
const res = await request.get('/cms/cms-article/data', {
const res = await request.get(CMS_API_BASE_URL + '/cms/cms-article/data', {
params
});
if (res.data.code === 0) {
@@ -139,7 +140,7 @@ export async function importArticles(file: File) {
const formData = new FormData();
formData.append('file', file);
const res = await request.post<ApiResult<unknown>>(
'/cms/cms-article/import',
CMS_API_BASE_URL + '/cms/cms-article/import',
formData
);
if (res.data.code === 0) {
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsArticleCategory, CmsArticleCategoryParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询文章分类表
*/
export async function pageCmsArticleCategory(params: CmsArticleCategoryParam) {
const res = await request.get<ApiResult<PageResult<CmsArticleCategory>>>(
MODULES_API_URL + '/cms/cms-article-category/page',
CMS_API_BASE_URL + '/cms/cms-article-category/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsArticleCategory(params: CmsArticleCategoryParam) {
*/
export async function listCmsArticleCategory(params?: CmsArticleCategoryParam) {
const res = await request.get<ApiResult<CmsArticleCategory[]>>(
MODULES_API_URL + '/cms/cms-article-category',
CMS_API_BASE_URL + '/cms/cms-article-category',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsArticleCategory(params?: CmsArticleCategoryParam) {
*/
export async function addCmsArticleCategory(data: CmsArticleCategory) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-category',
CMS_API_BASE_URL + '/cms/cms-article-category',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsArticleCategory(data: CmsArticleCategory) {
*/
export async function updateCmsArticleCategory(data: CmsArticleCategory) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-category',
CMS_API_BASE_URL + '/cms/cms-article-category',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsArticleCategory(data: CmsArticleCategory) {
*/
export async function removeCmsArticleCategory(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-category/' + id
CMS_API_BASE_URL + '/cms/cms-article-category/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleCategory(
data: (number | undefined)[]
) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-category/batch',
CMS_API_BASE_URL + '/cms/cms-article-category/batch',
{
data
}
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleCategory(
*/
export async function getCmsArticleCategory(id: number) {
const res = await request.get<ApiResult<CmsArticleCategory>>(
MODULES_API_URL + '/cms/cms-article-category/' + id
CMS_API_BASE_URL + '/cms/cms-article-category/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsArticleComment, CmsArticleCommentParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询文章评论表
*/
export async function pageCmsArticleComment(params: CmsArticleCommentParam) {
const res = await request.get<ApiResult<PageResult<CmsArticleComment>>>(
MODULES_API_URL + '/cms/cms-article-comment/page',
CMS_API_BASE_URL + '/cms/cms-article-comment/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsArticleComment(params: CmsArticleCommentParam) {
*/
export async function listCmsArticleComment(params?: CmsArticleCommentParam) {
const res = await request.get<ApiResult<CmsArticleComment[]>>(
MODULES_API_URL + '/cms/cms-article-comment',
CMS_API_BASE_URL + '/cms/cms-article-comment',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsArticleComment(params?: CmsArticleCommentParam) {
*/
export async function addCmsArticleComment(data: CmsArticleComment) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-comment',
CMS_API_BASE_URL + '/cms/cms-article-comment',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsArticleComment(data: CmsArticleComment) {
*/
export async function updateCmsArticleComment(data: CmsArticleComment) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-comment',
CMS_API_BASE_URL + '/cms/cms-article-comment',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsArticleComment(data: CmsArticleComment) {
*/
export async function removeCmsArticleComment(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-comment/' + id
CMS_API_BASE_URL + '/cms/cms-article-comment/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleComment(
data: (number | undefined)[]
) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-comment/batch',
CMS_API_BASE_URL + '/cms/cms-article-comment/batch',
{
data
}
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleComment(
*/
export async function getCmsArticleComment(id: number) {
const res = await request.get<ApiResult<CmsArticleComment>>(
MODULES_API_URL + '/cms/cms-article-comment/' + id
CMS_API_BASE_URL + '/cms/cms-article-comment/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsArticleContent, CmsArticleContentParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询文章记录表
*/
export async function pageCmsArticleContent(params: CmsArticleContentParam) {
const res = await request.get<ApiResult<PageResult<CmsArticleContent>>>(
MODULES_API_URL + '/cms/cms-article-content/page',
CMS_API_BASE_URL + '/cms/cms-article-content/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsArticleContent(params: CmsArticleContentParam) {
*/
export async function listCmsArticleContent(params?: CmsArticleContentParam) {
const res = await request.get<ApiResult<CmsArticleContent[]>>(
MODULES_API_URL + '/cms/cms-article-content',
CMS_API_BASE_URL + '/cms/cms-article-content',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsArticleContent(params?: CmsArticleContentParam) {
*/
export async function addCmsArticleContent(data: CmsArticleContent) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-content',
CMS_API_BASE_URL + '/cms/cms-article-content',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsArticleContent(data: CmsArticleContent) {
*/
export async function updateCmsArticleContent(data: CmsArticleContent) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-content',
CMS_API_BASE_URL + '/cms/cms-article-content',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsArticleContent(data: CmsArticleContent) {
*/
export async function removeCmsArticleContent(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-content/' + id
CMS_API_BASE_URL + '/cms/cms-article-content/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleContent(
data: (number | undefined)[]
) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-content/batch',
CMS_API_BASE_URL + '/cms/cms-article-content/batch',
{
data
}
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleContent(
*/
export async function getCmsArticleContent(id: number) {
const res = await request.get<ApiResult<CmsArticleContent>>(
MODULES_API_URL + '/cms/cms-article-content/' + id
CMS_API_BASE_URL + '/cms/cms-article-content/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsArticleCount, CmsArticleCountParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询点赞文章
*/
export async function pageCmsArticleCount(params: CmsArticleCountParam) {
const res = await request.get<ApiResult<PageResult<CmsArticleCount>>>(
MODULES_API_URL + '/cms/cms-article-count/page',
CMS_API_BASE_URL + '/cms/cms-article-count/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsArticleCount(params: CmsArticleCountParam) {
*/
export async function listCmsArticleCount(params?: CmsArticleCountParam) {
const res = await request.get<ApiResult<CmsArticleCount[]>>(
MODULES_API_URL + '/cms/cms-article-count',
CMS_API_BASE_URL + '/cms/cms-article-count',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsArticleCount(params?: CmsArticleCountParam) {
*/
export async function addCmsArticleCount(data: CmsArticleCount) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-count',
CMS_API_BASE_URL + '/cms/cms-article-count',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsArticleCount(data: CmsArticleCount) {
*/
export async function updateCmsArticleCount(data: CmsArticleCount) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-count',
CMS_API_BASE_URL + '/cms/cms-article-count',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsArticleCount(data: CmsArticleCount) {
*/
export async function removeCmsArticleCount(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-count/' + id
CMS_API_BASE_URL + '/cms/cms-article-count/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsArticleCount(id?: number) {
*/
export async function removeBatchCmsArticleCount(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-count/batch',
CMS_API_BASE_URL + '/cms/cms-article-count/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsArticleCount(data: (number | undefined)[]) {
*/
export async function getCmsArticleCount(id: number) {
const res = await request.get<ApiResult<CmsArticleCount>>(
MODULES_API_URL + '/cms/cms-article-count/' + id
CMS_API_BASE_URL + '/cms/cms-article-count/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsArticleLike, CmsArticleLikeParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询点赞文章
*/
export async function pageCmsArticleLike(params: CmsArticleLikeParam) {
const res = await request.get<ApiResult<PageResult<CmsArticleLike>>>(
MODULES_API_URL + '/cms/cms-article-like/page',
CMS_API_BASE_URL + '/cms/cms-article-like/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsArticleLike(params: CmsArticleLikeParam) {
*/
export async function listCmsArticleLike(params?: CmsArticleLikeParam) {
const res = await request.get<ApiResult<CmsArticleLike[]>>(
MODULES_API_URL + '/cms/cms-article-like',
CMS_API_BASE_URL + '/cms/cms-article-like',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsArticleLike(params?: CmsArticleLikeParam) {
*/
export async function addCmsArticleLike(data: CmsArticleLike) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-like',
CMS_API_BASE_URL + '/cms/cms-article-like',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsArticleLike(data: CmsArticleLike) {
*/
export async function updateCmsArticleLike(data: CmsArticleLike) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-like',
CMS_API_BASE_URL + '/cms/cms-article-like',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsArticleLike(data: CmsArticleLike) {
*/
export async function removeCmsArticleLike(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-like/' + id
CMS_API_BASE_URL + '/cms/cms-article-like/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsArticleLike(id?: number) {
*/
export async function removeBatchCmsArticleLike(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-article-like/batch',
CMS_API_BASE_URL + '/cms/cms-article-like/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsArticleLike(data: (number | undefined)[]) {
*/
export async function getCmsArticleLike(id: number) {
const res = await request.get<ApiResult<CmsArticleLike>>(
MODULES_API_URL + '/cms/cms-article-like/' + id
CMS_API_BASE_URL + '/cms/cms-article-like/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsDesign, CmsDesignParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询页面管理记录表
*/
export async function pageCmsDesign(params: CmsDesignParam) {
const res = await request.get<ApiResult<PageResult<CmsDesign>>>(
MODULES_API_URL + '/cms/cms-design/page',
CMS_API_BASE_URL + '/cms/cms-design/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsDesign(params: CmsDesignParam) {
*/
export async function listCmsDesign(params?: CmsDesignParam) {
const res = await request.get<ApiResult<CmsDesign[]>>(
MODULES_API_URL + '/cms/cms-design',
CMS_API_BASE_URL + '/cms/cms-design',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsDesign(params?: CmsDesignParam) {
*/
export async function addCmsDesign(data: CmsDesign) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-design',
CMS_API_BASE_URL + '/cms/cms-design',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsDesign(data: CmsDesign) {
*/
export async function updateCmsDesign(data: CmsDesign) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-design',
CMS_API_BASE_URL + '/cms/cms-design',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsDesign(data: CmsDesign) {
*/
export async function removeCmsDesign(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-design/' + id
CMS_API_BASE_URL + '/cms/cms-design/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsDesign(id?: number) {
*/
export async function removeBatchCmsDesign(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-design/batch',
CMS_API_BASE_URL + '/cms/cms-design/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsDesign(data: (number | undefined)[]) {
*/
export async function getCmsDesign(id: number) {
const res = await request.get<ApiResult<CmsDesign>>(
MODULES_API_URL + '/cms/cms-design/' + id
CMS_API_BASE_URL + '/cms/cms-design/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+11 -11
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsDomain, CmsDomainParam } from './model';
import { SERVER_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询网站域名记录表
*/
export async function pageCmsDomain(params: CmsDomainParam) {
const res = await request.get<ApiResult<PageResult<CmsDomain>>>(
SERVER_API_URL + '/cms/cms-domain/page',
CMS_API_BASE_URL + '/cms/cms-domain/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsDomain(params: CmsDomainParam) {
*/
export async function listCmsDomain(params?: CmsDomainParam) {
const res = await request.get<ApiResult<CmsDomain[]>>(
SERVER_API_URL + '/cms/cms-domain',
CMS_API_BASE_URL + '/cms/cms-domain',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsDomain(params?: CmsDomainParam) {
*/
export async function addCmsDomain(data: CmsDomain) {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/cms/cms-domain',
CMS_API_BASE_URL + '/cms/cms-domain',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsDomain(data: CmsDomain) {
*/
export async function updateCmsDomain(data: CmsDomain) {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/cms/cms-domain/domain',
CMS_API_BASE_URL + '/cms/cms-domain/domain',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsDomain(data: CmsDomain) {
*/
export async function removeCmsDomain(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
SERVER_API_URL + '/cms/cms-domain/' + id
CMS_API_BASE_URL + '/cms/cms-domain/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsDomain(id?: number) {
*/
export async function removeBatchCmsDomain(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
SERVER_API_URL + '/cms/cms-domain/batch',
CMS_API_BASE_URL + '/cms/cms-domain/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsDomain(data: (number | undefined)[]) {
*/
export async function getCmsDomain(id: number) {
const res = await request.get<ApiResult<CmsDomain>>(
SERVER_API_URL + '/cms/cms-domain/' + id
CMS_API_BASE_URL + '/cms/cms-domain/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -114,7 +114,7 @@ export async function checkExistence(
id?: number
) {
const res = await request.get<ApiResult<unknown>>(
SERVER_API_URL + '/cms/cms-domain/existence',
CMS_API_BASE_URL + '/cms/cms-domain/existence',
{
params: { field, value, id }
}
@@ -127,7 +127,7 @@ export async function checkExistence(
export async function resolvable(id: number) {
const res = await request.get<ApiResult<CmsDomain>>(
SERVER_API_URL + '/cms/cms-domain/resolvable/' + id
CMS_API_BASE_URL + '/cms/cms-domain/resolvable/' + id
);
if (res.data.code === 0 && res.data) {
return res.data;
@@ -143,7 +143,7 @@ export async function getAuthorizedDomain() {
return false;
}
const res = await request.get<ApiResult<CmsDomain>>(
SERVER_API_URL +
CMS_API_BASE_URL +
'/cms/cms-domain/getAuthorizedDomain/' +
localStorage.getItem('WebsiteId')
);
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsForm, CmsFormParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询表单设计表
*/
export async function pageCmsForm(params: CmsFormParam) {
const res = await request.get<ApiResult<PageResult<CmsForm>>>(
MODULES_API_URL + '/cms/cms-form/page',
CMS_API_BASE_URL + '/cms/cms-form/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsForm(params: CmsFormParam) {
*/
export async function listCmsForm(params?: CmsFormParam) {
const res = await request.get<ApiResult<CmsForm[]>>(
MODULES_API_URL + '/cms/cms-form',
CMS_API_BASE_URL + '/cms/cms-form',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsForm(params?: CmsFormParam) {
*/
export async function addCmsForm(data: CmsForm) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-form',
CMS_API_BASE_URL + '/cms/cms-form',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsForm(data: CmsForm) {
*/
export async function updateCmsForm(data: CmsForm) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-form',
CMS_API_BASE_URL + '/cms/cms-form',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsForm(data: CmsForm) {
*/
export async function removeCmsForm(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-form/' + id
CMS_API_BASE_URL + '/cms/cms-form/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsForm(id?: number) {
*/
export async function removeBatchCmsForm(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-form/batch',
CMS_API_BASE_URL + '/cms/cms-form/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsForm(data: (number | undefined)[]) {
*/
export async function getCmsForm(id: number) {
const res = await request.get<ApiResult<CmsForm>>(
MODULES_API_URL + '/cms/cms-form/' + id
CMS_API_BASE_URL + '/cms/cms-form/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsFormRecord, CmsFormRecordParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询表单数据记录表
*/
export async function pageCmsFormRecord(params: CmsFormRecordParam) {
const res = await request.get<ApiResult<PageResult<CmsFormRecord>>>(
MODULES_API_URL + '/cms/cms-form-record/page',
CMS_API_BASE_URL + '/cms/cms-form-record/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsFormRecord(params: CmsFormRecordParam) {
*/
export async function listCmsFormRecord(params?: CmsFormRecordParam) {
const res = await request.get<ApiResult<CmsFormRecord[]>>(
MODULES_API_URL + '/cms/cms-form-record',
CMS_API_BASE_URL + '/cms/cms-form-record',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsFormRecord(params?: CmsFormRecordParam) {
*/
export async function addCmsFormRecord(data: CmsFormRecord) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-form-record',
CMS_API_BASE_URL + '/cms/cms-form-record',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsFormRecord(data: CmsFormRecord) {
*/
export async function updateCmsFormRecord(data: CmsFormRecord) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-form-record',
CMS_API_BASE_URL + '/cms/cms-form-record',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsFormRecord(data: CmsFormRecord) {
*/
export async function removeCmsFormRecord(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-form-record/' + id
CMS_API_BASE_URL + '/cms/cms-form-record/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsFormRecord(id?: number) {
*/
export async function removeBatchCmsFormRecord(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-form-record/batch',
CMS_API_BASE_URL + '/cms/cms-form-record/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsFormRecord(data: (number | undefined)[]) {
*/
export async function getCmsFormRecord(id: number) {
const res = await request.get<ApiResult<CmsFormRecord>>(
MODULES_API_URL + '/cms/cms-form-record/' + id
CMS_API_BASE_URL + '/cms/cms-form-record/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsLang, CmsLangParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询国际化
*/
export async function pageCmsLang(params: CmsLangParam) {
const res = await request.get<ApiResult<PageResult<CmsLang>>>(
MODULES_API_URL + '/cms/cms-lang/page',
CMS_API_BASE_URL + '/cms/cms-lang/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsLang(params: CmsLangParam) {
*/
export async function listCmsLang(params?: CmsLangParam) {
const res = await request.get<ApiResult<CmsLang[]>>(
MODULES_API_URL + '/cms/cms-lang',
CMS_API_BASE_URL + '/cms/cms-lang',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsLang(params?: CmsLangParam) {
*/
export async function addCmsLang(data: CmsLang) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-lang',
CMS_API_BASE_URL + '/cms/cms-lang',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsLang(data: CmsLang) {
*/
export async function updateCmsLang(data: CmsLang) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-lang',
CMS_API_BASE_URL + '/cms/cms-lang',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsLang(data: CmsLang) {
*/
export async function removeCmsLang(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-lang/' + id
CMS_API_BASE_URL + '/cms/cms-lang/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsLang(id?: number) {
*/
export async function removeBatchCmsLang(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-lang/batch',
CMS_API_BASE_URL + '/cms/cms-lang/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsLang(data: (number | undefined)[]) {
*/
export async function getCmsLang(id: number) {
const res = await request.get<ApiResult<CmsLang>>(
MODULES_API_URL + '/cms/cms-lang/' + id
CMS_API_BASE_URL + '/cms/cms-lang/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsLangLog, CmsLangLogParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询国际化记录启用
*/
export async function pageCmsLangLog(params: CmsLangLogParam) {
const res = await request.get<ApiResult<PageResult<CmsLangLog>>>(
MODULES_API_URL + '/cms/cms-lang-log/page',
CMS_API_BASE_URL + '/cms/cms-lang-log/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsLangLog(params: CmsLangLogParam) {
*/
export async function listCmsLangLog(params?: CmsLangLogParam) {
const res = await request.get<ApiResult<CmsLangLog[]>>(
MODULES_API_URL + '/cms/cms-lang-log',
CMS_API_BASE_URL + '/cms/cms-lang-log',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsLangLog(params?: CmsLangLogParam) {
*/
export async function addCmsLangLog(data: CmsLangLog) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-lang-log',
CMS_API_BASE_URL + '/cms/cms-lang-log',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsLangLog(data: CmsLangLog) {
*/
export async function updateCmsLangLog(data: CmsLangLog) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-lang-log',
CMS_API_BASE_URL + '/cms/cms-lang-log',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsLangLog(data: CmsLangLog) {
*/
export async function removeCmsLangLog(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-lang-log/' + id
CMS_API_BASE_URL + '/cms/cms-lang-log/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsLangLog(id?: number) {
*/
export async function removeBatchCmsLangLog(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-lang-log/batch',
CMS_API_BASE_URL + '/cms/cms-lang-log/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsLangLog(data: (number | undefined)[]) {
*/
export async function getCmsLangLog(id: number) {
const res = await request.get<ApiResult<CmsLangLog>>(
MODULES_API_URL + '/cms/cms-lang-log/' + id
CMS_API_BASE_URL + '/cms/cms-lang-log/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+9 -9
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsLink, CmsLinkParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询常用链接
*/
export async function pageCmsLink(params: CmsLinkParam) {
const res = await request.get<ApiResult<PageResult<CmsLink>>>(
MODULES_API_URL + '/cms/cms-link/page',
CMS_API_BASE_URL + '/cms/cms-link/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsLink(params: CmsLinkParam) {
*/
export async function listCmsLink(params?: CmsLinkParam) {
const res = await request.get<ApiResult<CmsLink[]>>(
MODULES_API_URL + '/cms/cms-link',
CMS_API_BASE_URL + '/cms/cms-link',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsLink(params?: CmsLinkParam) {
*/
export async function addCmsLink(data: CmsLink) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-link',
CMS_API_BASE_URL + '/cms/cms-link',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsLink(data: CmsLink) {
*/
export async function updateCmsLink(data: CmsLink) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-link',
CMS_API_BASE_URL + '/cms/cms-link',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsLink(data: CmsLink) {
*/
export async function removeCmsLink(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-link/' + id
CMS_API_BASE_URL + '/cms/cms-link/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsLink(id?: number) {
*/
export async function updateBatchCmsLink(data: any) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-link/batch',
CMS_API_BASE_URL + '/cms/cms-link/batch',
data
);
if (res.data.code === 0) {
@@ -95,7 +95,7 @@ export async function updateBatchCmsLink(data: any) {
*/
export async function removeBatchCmsLink(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-link/batch',
CMS_API_BASE_URL + '/cms/cms-link/batch',
{
data
}
@@ -111,7 +111,7 @@ export async function removeBatchCmsLink(data: (number | undefined)[]) {
*/
export async function getCmsLink(id: number) {
const res = await request.get<ApiResult<CmsLink>>(
MODULES_API_URL + '/cms/cms-link/' + id
CMS_API_BASE_URL + '/cms/cms-link/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsModel, CmsModelParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询模型
*/
export async function pageCmsModel(params: CmsModelParam) {
const res = await request.get<ApiResult<PageResult<CmsModel>>>(
MODULES_API_URL + '/cms/cms-model/page',
CMS_API_BASE_URL + '/cms/cms-model/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsModel(params: CmsModelParam) {
*/
export async function listCmsModel(params?: CmsModelParam) {
const res = await request.get<ApiResult<CmsModel[]>>(
MODULES_API_URL + '/cms/cms-model',
CMS_API_BASE_URL + '/cms/cms-model',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsModel(params?: CmsModelParam) {
*/
export async function addCmsModel(data: CmsModel) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-model',
CMS_API_BASE_URL + '/cms/cms-model',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsModel(data: CmsModel) {
*/
export async function updateCmsModel(data: CmsModel) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-model',
CMS_API_BASE_URL + '/cms/cms-model',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsModel(data: CmsModel) {
*/
export async function removeCmsModel(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-model/' + id
CMS_API_BASE_URL + '/cms/cms-model/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsModel(id?: number) {
*/
export async function removeBatchCmsModel(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-model/batch',
CMS_API_BASE_URL + '/cms/cms-model/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsModel(data: (number | undefined)[]) {
*/
export async function getCmsModel(id: number) {
const res = await request.get<ApiResult<CmsModel>>(
MODULES_API_URL + '/cms/cms-model/' + id
CMS_API_BASE_URL + '/cms/cms-model/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+11 -11
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsNavigation, CmsNavigationParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询网站导航记录表
*/
export async function pageCmsNavigation(params: CmsNavigationParam) {
const res = await request.get<ApiResult<PageResult<CmsNavigation>>>(
MODULES_API_URL + '/cms/cms-navigation/page',
CMS_API_BASE_URL + '/cms/cms-navigation/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsNavigation(params: CmsNavigationParam) {
*/
export async function listCmsNavigation(params?: CmsNavigationParam) {
const res = await request.get<ApiResult<CmsNavigation[]>>(
MODULES_API_URL + '/cms/cms-navigation',
CMS_API_BASE_URL + '/cms/cms-navigation',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsNavigation(params?: CmsNavigationParam) {
*/
export async function treeNavigation(params?: CmsNavigationParam) {
const res = await request.get<ApiResult<CmsNavigation[]>>(
MODULES_API_URL + '/cms/cms-navigation/tree',
CMS_API_BASE_URL + '/cms/cms-navigation/tree',
{
params
}
@@ -55,7 +55,7 @@ export async function treeNavigation(params?: CmsNavigationParam) {
*/
export async function addCmsNavigation(data: CmsNavigation) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-navigation',
CMS_API_BASE_URL + '/cms/cms-navigation',
data
);
if (res.data.code === 0) {
@@ -69,7 +69,7 @@ export async function addCmsNavigation(data: CmsNavigation) {
*/
export async function updateCmsNavigation(data: CmsNavigation) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-navigation',
CMS_API_BASE_URL + '/cms/cms-navigation',
data
);
if (res.data.code === 0) {
@@ -83,7 +83,7 @@ export async function updateCmsNavigation(data: CmsNavigation) {
*/
export async function removeCmsNavigation(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-navigation/' + id
CMS_API_BASE_URL + '/cms/cms-navigation/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -96,7 +96,7 @@ export async function removeCmsNavigation(id?: number) {
*/
export async function removeBatchCmsNavigation(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-navigation/batch',
CMS_API_BASE_URL + '/cms/cms-navigation/batch',
{
data
}
@@ -112,7 +112,7 @@ export async function removeBatchCmsNavigation(data: (number | undefined)[]) {
*/
export async function getCmsNavigation(id: number) {
const res = await request.get<ApiResult<CmsNavigation>>(
MODULES_API_URL + '/cms/cms-navigation/' + id
CMS_API_BASE_URL + '/cms/cms-navigation/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -125,7 +125,7 @@ export async function getCmsNavigation(id: number) {
*/
export async function getByCode(code: string) {
const res = await request.get<ApiResult<CmsNavigation>>(
'/cms/cms-navigation/getByCode/' + code
CMS_API_BASE_URL + '/cms/cms-navigation/getByCode/' + code
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -140,7 +140,7 @@ export async function importCmsNavigation(file: File) {
const formData = new FormData();
formData.append('file', file);
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-navigation/import',
CMS_API_BASE_URL + '/cms/cms-navigation/import',
formData
);
if (res.data.code === 0) {
+8 -7
View File
@@ -1,4 +1,5 @@
import request from '@/utils/request';
import { CMS_API_BASE_URL } from '@/config/setting';
import type { ApiResult, PageResult } from '@/api';
import type { CmsOrder, CmsOrderParam } from './model';
@@ -7,7 +8,7 @@ import type { CmsOrder, CmsOrderParam } from './model';
*/
export async function pageCmsOrder(params: CmsOrderParam) {
const res = await request.get<ApiResult<PageResult<CmsOrder>>>(
'/cms/cms-order/page',
CMS_API_BASE_URL + '/cms/cms-order/page',
{
params
}
@@ -23,7 +24,7 @@ export async function pageCmsOrder(params: CmsOrderParam) {
*/
export async function listCmsOrder(params?: CmsOrderParam) {
const res = await request.get<ApiResult<CmsOrder[]>>(
'/cms/cms-order',
CMS_API_BASE_URL + '/cms/cms-order',
{
params
}
@@ -39,7 +40,7 @@ export async function listCmsOrder(params?: CmsOrderParam) {
*/
export async function addCmsOrder(data: CmsOrder) {
const res = await request.post<ApiResult<unknown>>(
'/cms/cms-order',
CMS_API_BASE_URL + '/cms/cms-order',
data
);
if (res.data.code === 0) {
@@ -53,7 +54,7 @@ export async function addCmsOrder(data: CmsOrder) {
*/
export async function updateCmsOrder(data: CmsOrder) {
const res = await request.put<ApiResult<unknown>>(
'/cms/cms-order',
CMS_API_BASE_URL + '/cms/cms-order',
data
);
if (res.data.code === 0) {
@@ -67,7 +68,7 @@ export async function updateCmsOrder(data: CmsOrder) {
*/
export async function removeCmsOrder(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/cms/cms-order/' + id
CMS_API_BASE_URL + '/cms/cms-order/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -80,7 +81,7 @@ export async function removeCmsOrder(id?: number) {
*/
export async function removeBatchCmsOrder(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/cms/cms-order/batch',
CMS_API_BASE_URL + '/cms/cms-order/batch',
{
data
}
@@ -96,7 +97,7 @@ export async function removeBatchCmsOrder(data: (number | undefined)[]) {
*/
export async function getCmsOrder(id: number) {
const res = await request.get<ApiResult<CmsOrder>>(
'/cms/cms-order/' + id
CMS_API_BASE_URL + '/cms/cms-order/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsStatistics, CmsStatisticsParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询站点统计信息表
*/
export async function pageCmsStatistics(params: CmsStatisticsParam) {
const res = await request.get<ApiResult<PageResult<CmsStatistics>>>(
MODULES_API_URL + '/cms/cms-statistics/page',
CMS_API_BASE_URL + '/cms/cms-statistics/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsStatistics(params: CmsStatisticsParam) {
*/
export async function listCmsStatistics(params?: CmsStatisticsParam) {
const res = await request.get<ApiResult<CmsStatistics[]>>(
MODULES_API_URL + '/cms/cms-statistics',
CMS_API_BASE_URL + '/cms/cms-statistics',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsStatistics(params?: CmsStatisticsParam) {
*/
export async function addCmsStatistics(data: CmsStatistics) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-statistics',
CMS_API_BASE_URL + '/cms/cms-statistics',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsStatistics(data: CmsStatistics) {
*/
export async function updateCmsStatistics(data: CmsStatistics) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-statistics',
CMS_API_BASE_URL + '/cms/cms-statistics',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsStatistics(data: CmsStatistics) {
*/
export async function removeCmsStatistics(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-statistics/' + id
CMS_API_BASE_URL + '/cms/cms-statistics/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsStatistics(id?: number) {
*/
export async function removeBatchCmsStatistics(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-statistics/batch',
CMS_API_BASE_URL + '/cms/cms-statistics/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsStatistics(data: (number | undefined)[]) {
*/
export async function getCmsStatistics(id: number) {
const res = await request.get<ApiResult<CmsStatistics>>(
MODULES_API_URL + '/cms/cms-statistics/' + id
CMS_API_BASE_URL + '/cms/cms-statistics/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsTemplate, CmsTemplateParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
* 分页查询网站模版
*/
export async function pageCmsTemplate(params: CmsTemplateParam) {
const res = await request.get<ApiResult<PageResult<CmsTemplate>>>(
MODULES_API_URL + '/cms/cms-template/page',
CMS_API_BASE_URL + '/cms/cms-template/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsTemplate(params: CmsTemplateParam) {
*/
export async function listCmsTemplate(params?: CmsTemplateParam) {
const res = await request.get<ApiResult<CmsTemplate[]>>(
MODULES_API_URL + '/cms/cms-template',
CMS_API_BASE_URL + '/cms/cms-template',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsTemplate(params?: CmsTemplateParam) {
*/
export async function addCmsTemplate(data: CmsTemplate) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-template',
CMS_API_BASE_URL + '/cms/cms-template',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsTemplate(data: CmsTemplate) {
*/
export async function updateCmsTemplate(data: CmsTemplate) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-template',
CMS_API_BASE_URL + '/cms/cms-template',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsTemplate(data: CmsTemplate) {
*/
export async function removeCmsTemplate(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-template/' + id
CMS_API_BASE_URL + '/cms/cms-template/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsTemplate(id?: number) {
*/
export async function removeBatchCmsTemplate(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-template/batch',
CMS_API_BASE_URL + '/cms/cms-template/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsTemplate(data: (number | undefined)[]) {
*/
export async function getCmsTemplate(id: number) {
const res = await request.get<ApiResult<CmsTemplate>>(
MODULES_API_URL + '/cms/cms-template/' + id
CMS_API_BASE_URL + '/cms/cms-template/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+9 -9
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsWebsite, CmsWebsiteParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
*
*/
export async function pageCmsWebsite(params: CmsWebsiteParam) {
const res = await request.get<ApiResult<PageResult<CmsWebsite>>>(
MODULES_API_URL + '/cms/cms-website/page',
CMS_API_BASE_URL + '/cms/cms-website/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsWebsite(params: CmsWebsiteParam) {
*/
export async function listCmsWebsite(params?: CmsWebsiteParam) {
const res = await request.get<ApiResult<CmsWebsite[]>>(
MODULES_API_URL + '/cms/cms-website',
CMS_API_BASE_URL + '/cms/cms-website',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsWebsite(params?: CmsWebsiteParam) {
*/
export async function addCmsWebsite(data: CmsWebsite) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website',
CMS_API_BASE_URL + '/cms/cms-website',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsWebsite(data: CmsWebsite) {
*/
export async function updateCmsWebsite(data: CmsWebsite) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website',
CMS_API_BASE_URL + '/cms/cms-website',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsWebsite(data: CmsWebsite) {
*/
export async function removeCmsWebsite(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website/' + id
CMS_API_BASE_URL + '/cms/cms-website/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsWebsite(id?: number) {
*/
export async function removeBatchCmsWebsite(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website/batch',
CMS_API_BASE_URL + '/cms/cms-website/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsWebsite(data: (number | undefined)[]) {
*/
export async function getCmsWebsite(id: number) {
const res = await request.get<ApiResult<CmsWebsite>>(
MODULES_API_URL + '/cms/cms-website/' + id
CMS_API_BASE_URL + '/cms/cms-website/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -110,7 +110,7 @@ export async function getCmsWebsite(id: number) {
*/
export async function removeSiteInfoCache(key?: string) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website/clearSiteInfo/' + key
CMS_API_BASE_URL + '/cms/cms-website/clearSiteInfo/' + key
);
if (res.data.code === 0) {
return res.data.message;
+12 -12
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsWebsiteField, CmsWebsiteFieldParam, Config } from './model';
import { MODULES_API_URL, TEMPLATE_ID } from '@/config/setting';
import { CMS_API_BASE_URL, TEMPLATE_ID } from '@/config/setting';
/**
*
*/
export async function pageCmsWebsiteField(params: CmsWebsiteFieldParam) {
const res = await request.get<ApiResult<PageResult<CmsWebsiteField>>>(
MODULES_API_URL + '/cms/cms-website-field/page',
CMS_API_BASE_URL + '/cms/cms-website-field/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsWebsiteField(params: CmsWebsiteFieldParam) {
*/
export async function listCmsWebsiteField(params?: CmsWebsiteFieldParam) {
const res = await request.get<ApiResult<CmsWebsiteField[]>>(
MODULES_API_URL + '/cms/cms-website-field',
CMS_API_BASE_URL + '/cms/cms-website-field',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsWebsiteField(params?: CmsWebsiteFieldParam) {
*/
export async function addCmsWebsiteField(data: CmsWebsiteField) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-field',
CMS_API_BASE_URL + '/cms/cms-website-field',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsWebsiteField(data: CmsWebsiteField) {
*/
export async function updateCmsWebsiteField(data: CmsWebsiteField) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-field',
CMS_API_BASE_URL + '/cms/cms-website-field',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsWebsiteField(data: CmsWebsiteField) {
*/
export async function removeCmsWebsiteField(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-field/' + id
CMS_API_BASE_URL + '/cms/cms-website-field/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeCmsWebsiteField(id?: number) {
*/
export async function removeBatchCmsWebsiteField(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-field/batch',
CMS_API_BASE_URL + '/cms/cms-website-field/batch',
{
data
}
@@ -97,7 +97,7 @@ export async function removeBatchCmsWebsiteField(data: (number | undefined)[]) {
*/
export async function getCmsWebsiteField(id: number) {
const res = await request.get<ApiResult<CmsWebsiteField>>(
MODULES_API_URL + '/cms/cms-website-field/' + id
CMS_API_BASE_URL + '/cms/cms-website-field/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -110,7 +110,7 @@ export async function getCmsWebsiteField(id: number) {
*/
export async function getCmsWebsiteFieldByCode(code: string) {
const res = await request.get<ApiResult<CmsWebsiteField>>(
MODULES_API_URL + '/cms/cms-website-field/getByCode/' + code
CMS_API_BASE_URL + '/cms/cms-website-field/getByCode/' + code
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -123,7 +123,7 @@ export async function getCmsWebsiteFieldByCode(code: string) {
*/
export async function undeleteWebsiteField(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/website-field/undelete/' + id
CMS_API_BASE_URL + '/cms/website-field/undelete/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -138,7 +138,7 @@ export async function importWebsiteField(file: File) {
const formData = new FormData();
formData.append('file', file);
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-field/import',
CMS_API_BASE_URL + '/cms/cms-website-field/import',
formData
);
if (res.data.code === 0) {
@@ -152,7 +152,7 @@ export async function importWebsiteField(file: File) {
*/
export async function configWebsiteField(params?: CmsWebsiteFieldParam) {
const res = await request.get<ApiResult<Config>>(
MODULES_API_URL + '/cms/cms-website-field/config',
CMS_API_BASE_URL + '/cms/cms-website-field/config',
{
params,
headers: {
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CmsWebsiteSetting, CmsWebsiteSettingParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
*
*/
export async function pageCmsWebsiteSetting(params: CmsWebsiteSettingParam) {
const res = await request.get<ApiResult<PageResult<CmsWebsiteSetting>>>(
MODULES_API_URL + '/cms/cms-website-setting/page',
CMS_API_BASE_URL + '/cms/cms-website-setting/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageCmsWebsiteSetting(params: CmsWebsiteSettingParam) {
*/
export async function listCmsWebsiteSetting(params?: CmsWebsiteSettingParam) {
const res = await request.get<ApiResult<CmsWebsiteSetting[]>>(
MODULES_API_URL + '/cms/cms-website-setting',
CMS_API_BASE_URL + '/cms/cms-website-setting',
{
params
}
@@ -40,7 +40,7 @@ export async function listCmsWebsiteSetting(params?: CmsWebsiteSettingParam) {
*/
export async function addCmsWebsiteSetting(data: CmsWebsiteSetting) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-setting',
CMS_API_BASE_URL + '/cms/cms-website-setting',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addCmsWebsiteSetting(data: CmsWebsiteSetting) {
*/
export async function updateCmsWebsiteSetting(data: CmsWebsiteSetting) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-setting',
CMS_API_BASE_URL + '/cms/cms-website-setting',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateCmsWebsiteSetting(data: CmsWebsiteSetting) {
*/
export async function removeCmsWebsiteSetting(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-setting/' + id
CMS_API_BASE_URL + '/cms/cms-website-setting/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -83,7 +83,7 @@ export async function removeBatchCmsWebsiteSetting(
data: (number | undefined)[]
) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/cms/cms-website-setting/batch',
CMS_API_BASE_URL + '/cms/cms-website-setting/batch',
{
data
}
@@ -99,7 +99,7 @@ export async function removeBatchCmsWebsiteSetting(
*/
export async function getCmsWebsiteSetting(id: number) {
const res = await request.get<ApiResult<CmsWebsiteSetting>>(
MODULES_API_URL + '/cms/cms-website-setting/' + id
CMS_API_BASE_URL + '/cms/cms-website-setting/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
+8 -8
View File
@@ -1,14 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Link, LinkParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
import { CMS_API_BASE_URL } from '@/config/setting';
/**
*
*/
export async function pageLink(params: LinkParam) {
const res = await request.get<ApiResult<PageResult<Link>>>(
MODULES_API_URL + '/oa/link/page',
CMS_API_BASE_URL + '/oa/link/page',
{
params
}
@@ -24,7 +24,7 @@ export async function pageLink(params: LinkParam) {
*/
export async function listLink(params?: LinkParam) {
const res = await request.get<ApiResult<Link[]>>(
MODULES_API_URL + '/oa/link',
CMS_API_BASE_URL + '/oa/link',
{
params
}
@@ -40,7 +40,7 @@ export async function listLink(params?: LinkParam) {
*/
export async function addLink(data: Link) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/oa/link',
CMS_API_BASE_URL + '/oa/link',
data
);
if (res.data.code === 0) {
@@ -54,7 +54,7 @@ export async function addLink(data: Link) {
*/
export async function updateLink(data: Link) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/oa/link',
CMS_API_BASE_URL + '/oa/link',
data
);
if (res.data.code === 0) {
@@ -68,7 +68,7 @@ export async function updateLink(data: Link) {
*/
export async function removeLink(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/oa/link/' + id
CMS_API_BASE_URL + '/oa/link/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -81,7 +81,7 @@ export async function removeLink(id?: number) {
*/
export async function removeBatchLink(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/oa/link/batch',
CMS_API_BASE_URL + '/oa/link/batch',
{
data
}
@@ -101,7 +101,7 @@ export async function checkExistence(
id?: number
) {
const res = await request.get<ApiResult<unknown>>(
MODULES_API_URL + '/oa/link/existence',
CMS_API_BASE_URL + '/oa/link/existence',
{
params: { field, value, id }
}
+19 -1
View File
@@ -1,7 +1,7 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import type { User } from '@/api/system/user/model';
import type { UpdatePasswordParam, NoticeResult } from './model';
import type { UpdatePasswordParam, ResetPasswordParam, NoticeResult } from './model';
import { SERVER_API_URL } from '@/config/setting';
import { Company } from '@/api/system/company/model';
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
@@ -108,6 +108,24 @@ export async function updatePassword(
return Promise.reject(new Error(res.data.message));
}
/**
* /"忘记密码"
* POST /resetPassword
*/
export async function resetPassword(
data: ResetPasswordParam
): Promise<string> {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/resetPassword',
data,
{ headers: { 'X-Tenant-Skip': '1' } }
);
if (res.data.code === 0) {
return res.data.message ?? '密码重置成功';
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
+17
View File
@@ -24,6 +24,23 @@ export interface UpdatePasswordParam {
oldPassword: string;
}
/**
* /
* phone + smsCode email + emailCode
*/
export interface ResetPasswordParam {
/** 手机找回时必填 */
phone?: string;
smsCode?: string;
/** 邮箱找回时必填 */
email?: string;
emailCode?: string;
/** 新密码 */
newPassword: string;
/** 确认新密码 */
confirmPassword: string;
}
/**
*
*/
+162
View File
@@ -9,6 +9,30 @@ import type {
} from './model';
import { User } from '@/api/system/user/model';
import { SERVER_API_URL } from '@/config/setting';
import type { LoginOutcome, TenantOption } from './model';
/**
* access_token / /
*/
function saveLoginState(result: LoginResult | undefined, remember?: boolean) {
if (!result) return;
if (result.access_token) {
setToken(result.access_token, remember);
}
const user = result.user;
if (user) {
if (user.tenantId != null) {
try {
localStorage.setItem('TenantId', String(user.tenantId));
} catch { /* ignore */ }
}
if (user.userId != null) {
try {
localStorage.setItem('UserId', String(user.userId));
} catch { /* ignore */ }
}
}
}
/**
*
@@ -31,6 +55,48 @@ export async function login(data: LoginParam) {
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function loginByEmail(data: LoginParam) {
const res = await request.post<ApiResult<LoginResult>>(
'https://server.websoft.top/api/loginByEmail',
data
);
if (res.data.code === 0) {
setToken(res.data.data?.access_token, data.remember);
if (res.data.data?.user) {
const user = res.data.data?.user;
localStorage.setItem('TenantId', String(user.tenantId));
localStorage.setItem('UserId', String(user.userId));
}
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* ID登录
*/
export async function loginByUserId(data: LoginParam) {
const res = await request.post<ApiResult<LoginResult>>(
'https://server.websoft.top/api/loginByUserId',
data
);
if (res.data.code === 0) {
setToken(res.data.data?.access_token, data.remember);
if (res.data.data?.user) {
const user = res.data.data?.user;
localStorage.setItem('TenantId', String(user.tenantId));
localStorage.setItem('UserId', String(user.userId));
}
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
@@ -62,6 +128,62 @@ export async function loginBySms(data: LoginParam) {
return Promise.reject(new Error(res.data.message));
}
/**
*
* POST /loginBySuperAdminSms { phone, code, tenantId }
* - ok
* - selectTenant loginBySelectTenant
*/
export async function loginBySuperAdminSms(
data: LoginParam
): Promise<LoginOutcome> {
const res = await request.post<ApiResult<LoginResult>>(
SERVER_API_URL + '/loginBySuperAdminSms',
{ phone: data.phone, code: data.smsCode, smsCode: data.smsCode, tenantId: data.tenantId }
);
if (res.data.code !== 0) {
return Promise.reject(new Error(res.data.message));
}
const result = res.data.data;
if (result?.tenants && result.tenants.length > 0) {
return {
status: 'selectTenant',
tenants: result.tenants as TenantOption[],
message: res.data.message || '请选择要登录的租户'
};
}
if (result?.user) {
saveLoginState(result, data.remember);
return { status: 'ok', user: result.user, message: res.data.message || '登录成功' };
}
return Promise.reject(new Error(res.data.message || '登录失败'));
}
/**
*
* POST /loginBySelectTenant { phone, code, tenantId }
*/
export async function loginBySelectTenant(data: {
phone: string;
code: string;
tenantId: number;
remember?: boolean;
}): Promise<LoginOutcome> {
const res = await request.post<ApiResult<LoginResult>>(
SERVER_API_URL + '/loginBySelectTenant',
{ phone: data.phone, code: data.code, smsCode: data.code, tenantId: data.tenantId }
);
if (res.data.code !== 0) {
return Promise.reject(new Error(res.data.message));
}
const result = res.data.data;
if (result?.user) {
saveLoginState(result, data.remember);
return { status: 'ok', user: result.user, message: res.data.message || '登录成功' };
}
return Promise.reject(new Error(res.data.message || '登录失败'));
}
/**
*
*/
@@ -76,6 +198,46 @@ export async function sendSmsCaptcha(data: LoginParam) {
return Promise.reject(new Error(res.data.message));
}
/**
*
* POST /sendSmsCaptchaByAdmin { phone }
* @param options.skipTenant true tenantId 使
*/
export async function sendSmsCaptchaByAdmin(
data: LoginParam,
options?: { skipTenant?: boolean }
) {
const res = await request.post<ApiResult<SmsCaptchaResult>>(
SERVER_API_URL + '/sendSmsCaptchaByAdmin',
data,
options?.skipTenant ? { headers: { 'X-Tenant-Skip': '1' } } : undefined
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 使
* POST /sendEmailCaptcha { email }
* @param options.skipTenant true tenantId 使
*/
export async function sendEmailCaptcha(
data: LoginParam,
options?: { skipTenant?: boolean }
) {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/sendEmailCaptcha',
data,
options?.skipTenant ? { headers: { 'X-Tenant-Skip': '1' } } : undefined
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
+28
View File
@@ -5,6 +5,10 @@ import type { User } from '@/api/system/user/model';
export interface LoginParam {
// 账号
username?: string;
// 邮箱
email?: string;
// 用户ID
userId?: string | number;
// 密码
password?: string;
// 租户id
@@ -15,6 +19,8 @@ export interface LoginParam {
phone?: string;
// 短信验证码
code?: string;
// 短信验证码(别名,与后端 LoginParam.code 对应)
smsCode?: string;
}
/**
@@ -25,8 +31,30 @@ export interface LoginResult {
access_token?: string;
// 用户信息
user?: User;
// 多租户场景下返回,此时 access_token / user 为 null
tenants?: TenantOption[];
}
/**
*
*/
export interface TenantOption {
tenantId?: number;
tenantName?: string;
userId?: number;
username?: string;
nickname?: string;
}
/**
*
* - ok
* - selectTenant
*/
export type LoginOutcome =
| { status: 'ok'; user: NonNullable<LoginResult['user']>; message: string }
| { status: 'selectTenant'; tenants: TenantOption[]; message: string };
/**
*
*/
@@ -12,10 +12,14 @@ export interface ShopExpressTemplateDetail {
templateId?: number;
/** 计费方式: true=按件, false=按重量 */
type?: boolean;
/** 省份ID */
/** 省份ID(旧单值字段,兼容历史数据) */
provinceId?: number;
/** 城市ID */
/** 城市ID(旧单值字段,兼容历史数据) */
cityId?: number;
/** 配送区域模式: 0=全国, 1=指定配送(白名单), 2=指定不发货(黑名单) */
regionMode?: number;
/** 地区编码列表(6位行政区划码,逗号分隔),regionMode=1或2时有效 */
regionIds?: string;
/** 首件数量/重量 */
firstNum?: string;
/** 首件价格 */
+15 -3
View File
@@ -31,8 +31,10 @@ export interface ShopGoods {
canExpress?: number;
// 商品分类
category?: string;
// 商品分类ID
// 商品分类ID(主分类,向后兼容)
categoryId?: number;
// 商品分类ID列表(方案B:可归属多个分类)
categoryIds?: number[];
parentName?: string;
categoryName?: string;
// 一级分类
@@ -44,9 +46,9 @@ export interface ShopGoods {
commissionRole?: number;
// 货架
position?: string;
// 套餐价格
// 进货价格
buyingPrice?: string;
// 商品价格
// 商城价
price?: string;
originPrice?: string;
// 销售价格
@@ -62,6 +64,10 @@ export interface ShopGoods {
memberMarketPrice?: string;
// 经销商价格
dealerPrice?: string;
// 价格区间最大值(多规格列表展示用,由后端按 SKU 计算;单规格/区间相等时为 undefined
priceMax?: string;
salePriceMax?: string;
dealerPriceMax?: string;
// 有赠品
buyingGift?: boolean;
// 有赠品
@@ -87,6 +93,8 @@ export interface ShopGoods {
gainIntegral?: number;
// 推荐
recommend?: number;
// 置顶(1置顶 0不置顶)
isTop?: number;
// 商户ID
merchantId?: number;
// 商户名称
@@ -143,6 +151,8 @@ export interface ShopGoods {
activityType?: number;
// 配送方式:0送上门 1限自提
deliveryMode?: number;
// 所属专区(逗号分隔的 sectionId 字符串,对应 shop_goods.section_ids 列)
sectionIds?: string;
}
export interface BathSet {
@@ -168,6 +178,8 @@ export interface ShopGoodsCollectParam {
export interface ShopGoodsParam extends PageParam {
parentId?: number;
categoryId?: number;
// 方案B:按多个分类查询,与单值 categoryId 二选一,多值时优先
categoryIds?: number[];
goodsId?: number;
status?: number;
goodsName?: string;
+30
View File
@@ -0,0 +1,30 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopGoodsBrowse, ShopGoodsBrowseParam } from './model';
/**
*
*/
export async function pageShopGoodsBrowse(params: ShopGoodsBrowseParam) {
const res = await request.get<ApiResult<PageResult<ShopGoodsBrowse>>>(
'/shop/shop-goods-browse/admin/page',
{ params }
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function removeShopGoodsBrowse(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-goods-browse/admin/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
@@ -0,0 +1,42 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface ShopGoodsBrowse {
id?: number;
userId?: number;
goodsId?: number;
tenantId?: number;
merchantId?: number;
/** 浏览次数 */
visitCount?: number;
/** 最后浏览时间 */
lastVisitTime?: string;
/** 浏览来源 */
browseSource?: string;
createTime?: string;
updateTime?: string;
/** 商品名称(关联查询) */
goodsName?: string;
/** 商品封面图(关联查询) */
goodsImage?: string;
/** 商城价(关联查询) */
price?: string;
/** 市场价(关联查询) */
salePrice?: string;
}
/**
*
*/
export interface ShopGoodsBrowseParam extends PageParam {
id?: number;
userId?: number;
goodsId?: number;
merchantId?: number;
tenantId?: number;
browseSource?: string;
startTime?: string;
endTime?: string;
}
+3 -1
View File
@@ -12,12 +12,14 @@ export interface ShopGoodsSku {
sku?: string;
// 商品图片
image?: string;
// 商品价格
// 商城价
price?: string;
// 市场价格
salePrice?: string;
// 成本价
cost?: string;
// 会员价
buyingPrice?: string;
// 库存
stock?: number;
// sku编码
+69
View File
@@ -150,3 +150,72 @@ export async function refundShopOrder(data: ShopOrder) {
}
return Promise.reject(new Error(res.data.message));
}
/**
* 线
* 线/
* @param id ID
* @param remarks
* @param paymentVoucher
*/
export async function confirmOfflinePayment(
id: number,
remarks?: string,
paymentVoucher?: string
) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-order/confirm-offline-payment/' + id,
null,
{ params: { remarks, paymentVoucher } }
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
* ShopOrderOverviewVo
* - todaySales total_price
* - todayOrders
* - todayUsers
* - couponUsedCount 使
*/
export interface ShopOrderOverview {
/** 今日销售额(元),含未支付,按订单面额 total_price 求和 */
todaySales: number;
/** 今日订单数,含未支付 */
todayOrders: number;
/** 今日新增用户数 */
todayUsers: number;
/** 今日使用优惠券的订单数 */
couponUsedCount: number;
}
/**
* total_price
* /shop/shop-order/total pay_status total_price
*/
export async function shopOrderTotalAll() {
const res = await request.get<ApiResult<number>>(
MODULES_API_URL + '/shop/shop-order/total-all'
);
if (res.data.code === 0) {
return res.data.data ?? 0;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function shopOrderOverview() {
const res = await request.get<ApiResult<ShopOrderOverview>>(
MODULES_API_URL + '/shop/shop-order/overview'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+14 -4
View File
@@ -85,9 +85,9 @@ export interface ShopOrder {
coachId?: number;
// 支付的用户id
payUserId?: number;
// 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9~18 已废弃
// 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9线下付款, 10~18 已废弃
payType?: number;
// 代付支付方式, 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9~18 已废弃
// 代付支付方式, 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9线下付款, 10~18 已废弃
friendPayType?: number;
// 0未付款,1已付款
payStatus?: number;
@@ -111,6 +111,14 @@ export interface ShopOrder {
sendPhone?: string;
// 发货地址
sendAddress?: string;
// 发货门店id(快递配送=发货仓库,商家送货=配送门店)
sendStoreId?: number;
// 配送店员id(商家送货时记录具体配送人员)
deliveryUserId?: number;
// 发货门店名称(后端联表返回)
sendStoreName?: string;
// 配送店员名称(后端联表返回)
deliveryUserName?: string;
// 优惠类型:0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡,5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡
couponType?: number;
// 优惠说明
@@ -129,6 +137,8 @@ export interface ShopOrder {
invoiceNo?: string;
// 支付时间
payTime?: string;
// 线下收款支付凭证(图片地址,确认线下收款时上传)
paymentVoucher?: string;
// 退款时间
refundTime?: string;
// 申请退款时间
@@ -142,9 +152,9 @@ export interface ShopOrder {
// 系统版本号 0当前版本 value=其他版本
version?: number;
// 买家备注
buyerRemarks: undefined;
buyerRemarks?: string;
// 商家备注
merchantRemarks: undefined;
merchantRemarks?: string;
// 用户id
userId?: number;
// 备注
+74
View File
@@ -0,0 +1,74 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import { MODULES_API_URL } from '@/config/setting';
import type {
ShopOrderStatsOverview,
ShopOrderTrendItem,
ShopGoodsRankItem,
ShopOrderStatusDist,
StatsRangeParams
} from './model';
/**
* KPI
*/
export async function getShopOrderStatsOverview(
params: StatsRangeParams
): Promise<ShopOrderStatsOverview> {
const res = await request.get<ApiResult<ShopOrderStatsOverview>>(
MODULES_API_URL + '/shop/shop-order/stats/overview',
{ params }
);
if (res.data.code === 0) {
return res.data.data as ShopOrderStatsOverview;
}
return Promise.reject(new Error(res.data.message));
}
/**
* type=day|week|month
*/
export async function getShopOrderStatsTrend(
params: StatsRangeParams & { type?: string }
): Promise<ShopOrderTrendItem[]> {
const res = await request.get<ApiResult<ShopOrderTrendItem[]>>(
MODULES_API_URL + '/shop/shop-order/stats/trend',
{ params }
);
if (res.data.code === 0) {
return res.data.data as ShopOrderTrendItem[];
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function getShopOrderGoodsRank(
params: StatsRangeParams & { limit?: number }
): Promise<ShopGoodsRankItem[]> {
const res = await request.get<ApiResult<ShopGoodsRankItem[]>>(
MODULES_API_URL + '/shop/shop-order/stats/goods-rank',
{ params }
);
if (res.data.code === 0) {
return res.data.data as ShopGoodsRankItem[];
}
return Promise.reject(new Error(res.data.message));
}
/**
* /退
*/
export async function getShopOrderStatsStatusDist(
params: StatsRangeParams
): Promise<ShopOrderStatusDist> {
const res = await request.get<ApiResult<ShopOrderStatusDist>>(
MODULES_API_URL + '/shop/shop-order/stats/status-dist',
{ params }
);
if (res.data.code === 0) {
return res.data.data as ShopOrderStatusDist;
}
return Promise.reject(new Error(res.data.message));
}
+65
View File
@@ -0,0 +1,65 @@
/**
* -
*/
/** 经营概览(GMV + 实收双口径) */
export interface ShopOrderStatsOverview {
/** GMV(含未支付),按订单面额 total_price 求和 */
gmvSales: number;
/** 实收金额(仅已支付),按 pay_price 求和 */
paidSales: number;
/** 订单总数(含未支付) */
orderCount: number;
/** 已支付订单数 */
paidOrderCount: number;
/** 客单价 = 实收 / 已支付订单数 */
customerUnitPrice: number;
/** 退款金额 */
refundAmount: number;
/** 区间新增会员数 */
newUserCount: number;
/** 使用优惠券的订单数 */
couponUsedCount: number;
}
/** 销售趋势周期项 */
export interface ShopOrderTrendItem {
/** 周期:day=yyyy-MM-dd / week=yyyy-ww / month=yyyy-MM */
period: string;
gmvSales: number;
paidSales: number;
orderCount: number;
paidOrderCount: number;
}
/** 商品销量排行项 */
export interface ShopGoodsRankItem {
goodsId: number;
goodsName: string;
totalNum: number;
totalAmount: number;
}
/** 订单状态分布项 */
export interface ShopOrderStatusItem {
status: number;
statusName: string;
count: number;
}
/** 订单/退款分布 */
export interface ShopOrderStatusDist {
statusCounts: ShopOrderStatusItem[];
orderCount: number;
paidOrderCount: number;
refundCount: number;
refundAmount: number;
/** 退款率(% */
refundRate: number;
}
/** 区间参数(start/end 格式 yyyy-MM-dd HH:mm:ss */
export interface StatsRangeParams {
start: string;
end: string;
}
+2
View File
@@ -77,6 +77,8 @@ export interface ShopSetting {
payAlipayPrivateKey?: string;
payAlipayPublicKey?: string;
payBalanceEnabled?: boolean | number;
salesAccumulateType?: number;
}
/**
+258
View File
@@ -0,0 +1,258 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type {
HomeSection,
HomeSectionParam,
SectionPermission,
SectionGoods,
SectionSalesStatsVO
} from './model';
import type { User } from '@/api/system/user/model';
import { MODULES_API_URL } from '@/config/setting';
/**
*
*/
export async function pageHomeSections(params: HomeSectionParam) {
const res = await request.get<ApiResult<PageResult<HomeSection>>>(
MODULES_API_URL + '/shop/shop-home-section/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function listHomeSections(params?: HomeSectionParam) {
const res = await request.get<ApiResult<HomeSection[]>>(
MODULES_API_URL + '/shop/shop-home-section',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* id查询首页专区
*/
export async function getHomeSection(id: number) {
const res = await request.get<ApiResult<HomeSection>>(
MODULES_API_URL + '/shop/shop-home-section/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function addHomeSection(data: HomeSection) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function updateHomeSection(data: HomeSection) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function removeHomeSection(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* userId/realName/mobile/nickname/organizationName
*/
export async function listSectionUsers(id: number) {
const res = await request.get<ApiResult<User[]>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function setSectionUsers(id: number, userIds: number[]) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
userIds
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* /
*/
export async function addSectionUsers(id: number, userIds: number[]) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
userIds
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* /
*/
export async function removeSectionUsers(id: number, userIds: number[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
{ data: userIds }
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function listSectionGoods(
id: number,
params?: { page?: number; limit?: number }
) {
const res = await request.get<ApiResult<PageResult<SectionGoods>>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
{
params: { page: 1, limit: 10, ...params }
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function addSectionGoods(id: number, goodsIds: number[]) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
goodsIds
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
*
*/
export async function removeSectionGoods(id: number, goodsIds: number[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
{ data: goodsIds }
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 访
*/
export async function checkSectionPermission(sectionIds: number[]) {
const res = await request.post<ApiResult<SectionPermission[]>>(
MODULES_API_URL + '/shop/shop-home-section/check-permission',
{ sectionIds }
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* <img src> objectURL
* @param id ID
* @param envVersion release() / trial() / develop()
*/
export async function getSectionQrcode(
id: number,
envVersion: string = 'release'
): Promise<string> {
const res = await request.get(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/qrcode',
{ responseType: 'blob', params: { envVersion } } as any
);
const blob = res.data as Blob;
// 微信/后端出错时会返回 JSON 而非图片,这里解析出错误信息
if (blob && blob.type && blob.type.indexOf('image') === -1) {
let msg = '生成小程序码失败';
try {
const json = JSON.parse(await blob.text());
if (json && json.error) msg = json.error;
} catch (e) {
/* 忽略解析失败 */
}
return Promise.reject(new Error(msg));
}
return window.URL.createObjectURL(blob);
}
/**
* ( + + ), start/end(yyyy-MM-dd HH:mm:ss)
*/
export async function getSectionSalesStats(
id: number,
params?: { start?: string; end?: string }
) {
const res = await request.get<ApiResult<SectionSalesStatsVO>>(
MODULES_API_URL + '/shop/shop-home-section/' + id + '/stats',
{ params }
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+122
View File
@@ -0,0 +1,122 @@
import type { PageParam } from '@/api';
/**
*
*/
export interface HomeSection {
// 专区ID
sectionId?: number;
// 租户id
tenantId?: number;
// 专区标题
title?: string;
// 专区副标题
subtitle?: string;
// banner 图片URL
banner?: string;
// 关联分类ID(逗号分隔字符串,可选)
categoryIds?: string;
// 样式类型 0默认
styleType?: number;
// 是否受限专区 0否 1是
restricted?: number;
// 专区商城价折扣系数, 成交价=市场价×系数, 默认1.00不打折
discountRate?: number;
// 排序号(数字越小越靠前)
sortNumber?: number;
// 状态 0正常 1禁用
status?: number;
// 生效开始时间
startTime?: string;
// 生效结束时间
endTime?: string;
// 用户ID
userId?: number;
// 是否删除 0否 1是
deleted?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
// 销量件数(统计, 非DB字段)
salesNum?: number;
// 销售额(统计, 非DB字段)
salesAmount?: number;
}
/**
*
*/
export interface HomeSectionParam extends PageParam {
// 专区标题(模糊)
title?: string;
// 状态 0正常 1禁用
status?: number;
// 是否受限专区 0/1
restricted?: number;
// 排序号
sortNumber?: number;
}
/**
*
*/
export interface SectionPermission {
// 专区ID
sectionId?: number;
// 是否受限专区
restricted?: boolean;
// 当前用户是否允许访问
allowed?: boolean;
}
/**
*
*/
export interface SectionGoods {
goodsId?: number;
name?: string;
image?: string;
price?: string;
salePrice?: string;
stock?: number;
status?: number;
[key: string]: any;
}
/**
*
*/
export interface SectionUser {
id?: number;
sectionId?: number;
userId?: number;
}
/**
* ()
*/
export interface SectionSalesStatsVO {
// 专区ID
sectionId?: number;
// 销量件数
salesNum?: number;
// 销售额
salesAmount?: number;
// 统计开始时间
startTime?: string;
// 统计结束时间
endTime?: string;
// 商品销量排行
goodsRank?: SectionGoodsRankItem[];
}
/**
*
*/
export interface SectionGoodsRankItem {
goodsId?: number;
goodsName?: string;
salesNum?: number;
salesAmount?: number;
}
+1 -1
View File
@@ -181,7 +181,7 @@ export async function initialization(roleId?: number) {
* code查询租户
*/
export async function getByCode(code: string) {
const res = await request.get<Tenant>(
const res = await request.get<ApiResult<Tenant>>(
SERVER_API_URL + '/system/tenant/getByCode/' + code
);
if (res.data.code === 0 && res.data.data) {
+1 -1
View File
@@ -170,7 +170,7 @@ export async function updateUserStatus(userId?: number, status?: number) {
/**
*
*/
export async function updateUserRecommend(form) {
export async function updateUserRecommend(form: { userId: number; recommend: number }) {
const res = await request.put<ApiResult<unknown>>(
SERVER_API_URL + '/system/user/recommend',
form
+6
View File
@@ -84,6 +84,12 @@
label: '货到付款',
key: 'codPay',
icon: 'IdcardOutlined'
},
{
value: 9,
label: '线下付款',
key: 'offlinePay',
icon: 'IdcardOutlined'
}
]);
+362
View File
@@ -0,0 +1,362 @@
<!-- +市两级多选穿梭弹窗 -->
<template>
<div class="region-multi-select">
<!-- 触发区 -->
<div class="rms-trigger">
<a-button class="rms-trigger-btn" @click="openModal">
{{ triggerText }}
</a-button>
<span v-if="modelValue.length" class="rms-summary">
已选 {{ modelValue.length }} 个城市
</span>
</div>
<!-- 弹窗 -->
<a-modal
v-model:visible="visible"
title="区域选择"
width="760px"
:mask-closable="false"
@cancel="handleCancel"
>
<div class="rms-body">
<!-- 左侧候选 -->
<div class="rms-panel">
<a-input-search
v-model:value="keyword"
placeholder="输入地区关键字"
allow-clear
class="rms-search"
/>
<div class="rms-scroll">
<div
v-for="p in filteredProvinces"
:key="p.value"
class="rms-province"
>
<div class="rms-province-row">
<span class="rms-expand" @click="toggleExpand(p.value)">
{{ isExpanded(p.value) ? '▾' : '▸' }}
</span>
<span class="rms-label" @click="toggleExpand(p.value)">
{{ p.label }}
</span>
<a-button
type="link"
size="small"
:disabled="isProvinceSelected(p)"
@click="selectProvince(p)"
>
{{ isProvinceSelected(p) ? '已选' : '选择' }}
</a-button>
</div>
<div v-if="isExpanded(p.value)" class="rms-city-list">
<div
v-for="c in p.cities"
:key="c.value"
class="rms-city-row"
>
<span class="rms-label">{{ c.label }}</span>
<a-button
type="link"
size="small"
:disabled="isCitySelected(c.value)"
@click="toggleCity(c.value)"
>
{{ isCitySelected(c.value) ? '已选' : '选择' }}
</a-button>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧已选 -->
<div class="rms-panel">
<div class="rms-selected-title">
已选择 ({{ selectedCities.length }})
</div>
<div class="rms-scroll">
<div
v-for="item in selectedList"
:key="item.cityValue"
class="rms-selected-item"
>
<span class="rms-selected-label">
{{ item.provinceLabel }} / {{ item.cityLabel }}
</span>
<a-button
type="link"
size="small"
danger
@click="removeCity(item.cityValue)"
>
取消
</a-button>
</div>
<a-empty
v-if="!selectedCities.length"
:image="simpleImage"
description="暂未选择"
/>
</div>
</div>
</div>
<template #footer>
<a-button @click="selectAll">全部选择</a-button>
<span class="rms-footer-spacer"></span>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleConfirm">确认</a-button>
</template>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { Empty } from 'ant-design-vue';
import { getRegionsData } from '@/components/RegionsSelect/load-data';
const props = withDefaults(
defineProps<{
/** 已选城市ID数组(逗号分隔的行政区码) */
value?: string[];
placeholder?: string;
}>(),
{ value: () => [], placeholder: '选择配送地区' }
);
const emit = defineEmits<{
(e: 'update:value', value: string[]): void;
}>();
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
const visible = ref(false);
const keyword = ref('');
const expandedSet = ref<Set<string>>(new Set());
const selectedSet = ref<Set<string>>(new Set());
interface CityItem {
value: string;
label: string;
}
interface ProvinceItem {
value: string;
label: string;
cities: CityItem[];
}
const provinceList = ref<ProvinceItem[]>([]);
const cityToProvince = ref<Record<string, string>>({});
const labelMap = ref<Record<string, string>>({});
// +
getRegionsData().then((data) => {
const list: ProvinceItem[] = [];
const c2p: Record<string, string> = {};
const lm: Record<string, string> = {};
(data ?? []).forEach((p) => {
lm[p.value] = p.label;
const cities: CityItem[] = (p.children ?? []).map((c) => {
c2p[c.value] = p.value;
lm[c.value] = c.label;
return { value: c.value, label: c.label };
});
list.push({ value: p.value, label: p.label, cities });
});
provinceList.value = list;
cityToProvince.value = c2p;
labelMap.value = lm;
});
const modelValue = computed(() => props.value ?? []);
const triggerText = computed(() =>
modelValue.value.length
? `已选 ${modelValue.value.length} 个城市`
: props.placeholder
);
const isExpanded = (v: string) => expandedSet.value.has(v);
const toggleExpand = (v: string) => {
const s = new Set(expandedSet.value);
if (s.has(v)) {
s.delete(v);
} else {
s.add(v);
}
expandedSet.value = s;
};
const isCitySelected = (v: string) => selectedSet.value.has(v);
const isProvinceSelected = (p: ProvinceItem) =>
p.cities.length > 0 && p.cities.every((c) => selectedSet.value.has(c.value));
const filteredProvinces = computed(() => {
const kw = keyword.value.trim();
if (!kw) {
return provinceList.value;
}
return provinceList.value
.map((p) => {
const matchProvince = p.label.includes(kw);
if (matchProvince) {
return p;
}
const cities = p.cities.filter((c) => c.label.includes(kw));
if (cities.length) {
return { ...p, cities };
}
return null;
})
.filter(Boolean) as ProvinceItem[];
});
const selectedCities = computed(() => Array.from(selectedSet.value));
const selectedList = computed(() =>
selectedCities.value.map((cv) => ({
cityValue: cv,
cityLabel: labelMap.value[cv] || cv,
provinceValue: cityToProvince.value[cv],
provinceLabel: labelMap.value[cityToProvince.value[cv]] || ''
}))
);
const selectProvince = (p: ProvinceItem) => {
const s = new Set(selectedSet.value);
p.cities.forEach((c) => s.add(c.value));
selectedSet.value = s;
};
const toggleCity = (cv: string) => {
const s = new Set(selectedSet.value);
if (s.has(cv)) {
s.delete(cv);
} else {
s.add(cv);
}
selectedSet.value = s;
};
const removeCity = (cv: string) => {
const s = new Set(selectedSet.value);
s.delete(cv);
selectedSet.value = s;
};
const selectAll = () => {
const s = new Set<string>();
provinceList.value.forEach((p) =>
p.cities.forEach((c) => s.add(c.value))
);
selectedSet.value = s;
};
const openModal = () => {
selectedSet.value = new Set(modelValue.value);
keyword.value = '';
visible.value = true;
};
const handleConfirm = () => {
emit('update:value', selectedCities.value);
visible.value = false;
};
const handleCancel = () => {
visible.value = false;
};
//
watch(
() => props.value,
(v) => {
if (!visible.value) {
selectedSet.value = new Set(v ?? []);
}
}
);
</script>
<style scoped>
.region-multi-select {
width: 100%;
}
.rms-trigger {
display: flex;
align-items: center;
gap: 12px;
}
.rms-trigger-btn {
flex: 1;
text-align: left;
}
.rms-summary {
color: var(--ant-color-text-secondary, #999);
font-size: 12px;
white-space: nowrap;
}
.rms-body {
display: flex;
gap: 16px;
height: 420px;
}
.rms-panel {
flex: 1;
border: 1px solid var(--ant-color-border, #f0f0f0);
border-radius: 6px;
padding: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.rms-search {
margin-bottom: 12px;
}
.rms-scroll {
flex: 1;
overflow-y: auto;
}
.rms-province-row {
display: flex;
align-items: center;
padding: 4px 0;
}
.rms-expand {
width: 20px;
cursor: pointer;
user-select: none;
color: var(--ant-color-text-secondary, #999);
}
.rms-label {
flex: 1;
cursor: default;
}
.rms-city-list {
padding-left: 24px;
}
.rms-city-row {
display: flex;
align-items: center;
padding: 2px 0;
}
.rms-selected-title {
font-weight: 600;
margin-bottom: 8px;
}
.rms-selected-item {
display: flex;
align-items: center;
padding: 4px 0;
border-bottom: 1px dashed var(--ant-color-border, #f5f5f5);
}
.rms-selected-label {
flex: 1;
}
.rms-footer-spacer {
flex: 1;
}
</style>
+205
View File
@@ -0,0 +1,205 @@
<template>
<div class="rich-text-editor">
<!-- 编辑器尚未初始化时的占位 -->
<div
v-if="!editorReady"
:style="{ height: editorHeight + 'px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }"
>
编辑器加载中...
</div>
<template v-else>
<component
:is="Toolbar"
v-if="editorRef"
:editor="editorRef"
:defaultConfig="toolbarConfig"
style="border-bottom: 1px solid #d9d9d9"
/>
<component
:is="Editor"
:defaultConfig="editorConfig"
:modelValue="modelValue"
@onCreated="handleCreated"
@onChange="handleChange"
:style="{ height: editorHeight + 'px', overflowY: 'hidden' }"
/>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
import { message } from 'ant-design-vue'
const editorReady = ref(false)
const editorRef = shallowRef<any>()
const Toolbar = shallowRef<any>(null)
const Editor = shallowRef<any>(null)
const props = withDefaults(
defineProps<{
modelValue?: string
placeholder?: string
/** 编辑器高度(px),默认 380 */
height?: number
/** 图片上传大小限制(MB),默认 5 */
maxImageSize?: number
}>(),
{
modelValue: '',
placeholder: '请输入正文...',
height: 380,
maxImageSize: 5
}
)
const editorHeight = computed(() => props.height)
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
//
const toolbarConfig: Record<string, any> = {
//
excludeKeys: ['group-video']
}
/** 上传图片并返回 URL */
async function doUploadImage(file: File): Promise<string> {
if (!file.type.startsWith('image/')) {
message.error('只能上传图片文件')
return ''
}
if (file.size / 1024 / 1024 > props.maxImageSize) {
message.error(`图片大小不能超过 ${props.maxImageSize}MB`)
return ''
}
// uploadFile
const { uploadFile } = await import('@/api/system/file')
const res = await uploadFile(file)
return res.url || res.path || ''
}
/** 在编辑器当前光标处插入图片 */
function insertEditorImage(editor: any, url: string, alt = '') {
if (!editor || !url) return
try {
if (typeof editor.focus === 'function') editor.focus()
if (typeof editor.insertNode === 'function') {
editor.insertNode({
type: 'image',
src: url,
alt,
href: url,
children: [{ text: '' }]
})
} else if (typeof editor.dangerouslyInsertHtml === 'function') {
editor.dangerouslyInsertHtml(`<img src="${url}" alt="${alt}" style="max-width:100%;"/>`)
}
} catch (e) {
message.error('图片插入失败')
}
}
//
const editorConfig: Record<string, any> = {
placeholder: props.placeholder,
MENU_CONF: {
uploadImage: {
async customUpload(file: File, insertFn: (url: string, alt?: string, href?: string) => void) {
try {
const url = await doUploadImage(file)
if (url) insertFn(url, file.name || 'image', url)
} catch (e: any) {
message.error(e?.message || '图片上传失败')
}
}
}
},
customPaste(editor: any, event: ClipboardEvent) {
const items = event.clipboardData?.items
if (!items || items.length === 0) return true
const imageFiles: File[] = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (!item) continue
if (item.kind === 'file' && item.type.startsWith('image/')) {
const file = item.getAsFile()
if (file) imageFiles.push(file)
}
}
if (imageFiles.length === 0) return true
// base64
event.preventDefault()
//
Promise.all(
imageFiles.map(async (file) => {
const url = await doUploadImage(file)
return { file, url }
})
).then((results) => {
results.forEach(({ url }) => {
if (url) insertEditorImage(editor, url, '粘贴图片')
})
})
return false
}
}
function handleCreated(editor: any) {
editorRef.value = editor
}
function handleChange(editor: any) {
emit('update:modelValue', editor.getHtml())
}
/** 在光标处插入 HTML(供「插入附件」等外部入口调用) */
function insertHtml(html: string) {
const editor = editorRef.value
if (editor && typeof editor.dangerouslyInsertHtml === 'function') {
editor.dangerouslyInsertHtml(html)
}
}
defineExpose({ insertHtml })
//
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor && typeof editor.destroy === 'function') {
try {
editor.destroy()
} catch {
//
}
}
editorRef.value = null
})
// WangEditorCSS +
onMounted(async () => {
// CSS
const [cssModule, wangEditorModule] = await Promise.all([
import('@wangeditor/editor/dist/css/style.css'),
import('@wangeditor/editor-for-vue')
])
Toolbar.value = wangEditorModule.Toolbar
Editor.value = wangEditorModule.Editor
editorReady.value = true
})
</script>
<style scoped>
.rich-text-editor {
border: 1px solid #d9d9d9;
border-radius: 8px;
overflow: hidden;
}
</style>
+3
View File
@@ -21,6 +21,9 @@ export const FILE_SERVER =
*/
// 接口地址
export const API_BASE_URL: string = import.meta.env.VITE_API_URL;
// CMS 模块接口地址(cms 已迁移到 mp-java 并独立部署,调用指向远程服务)
export const CMS_API_BASE_URL: string =
import.meta.env.VITE_CMS_API_URL || 'https://cms-api.websoft.top/api';
export const PROJECT_NAME: string = import.meta.env.VITE_APP_NAME;
// 不显示侧栏的路由
export const HIDE_SIDEBARS: string[] = ['/home'];
+32 -1
View File
@@ -10,5 +10,36 @@ export default {
loading: 'loading',
oldPassword: 'Old',
newPassword: 'New',
confirm: 'Confirm'
confirm: 'Confirm',
forgetPage: {
title: 'Forgot Password',
subTitle: 'Enter your account info to reset the password',
tabEmail: 'By Email',
tabPhone: 'By Phone',
email: 'Email',
phone: 'Phone',
emailCode: 'Email Code',
code: 'SMS Code',
getCode: 'Get Code',
sentEmail: 'Verification code sent, please check your email',
sentPhone: 'Verification code sent',
newPassword: 'New Password',
confirmPassword: 'Confirm Password',
reset: 'Reset Password',
backLogin: 'Back to Login',
success: 'Password reset successfully, please login with the new password',
emailPlaceholder: 'Please enter your registered email',
phonePlaceholder: 'Please enter your registered phone',
codePlaceholder: 'Please enter the code',
passwordPlaceholder: 'At least 8 chars, must contain letters and numbers',
confirmPlaceholder: 'Please enter the password again',
newPasswordRule: 'Password must be at least 8 chars with letters and numbers',
confirmRule: 'The two passwords do not match',
emailRule: 'Please enter a valid email address',
phoneRule: 'Please enter a valid phone number',
codeRule: 'Please enter the code',
captchaTitle: 'Send Code',
captchaPlaceholder: 'Please enter the captcha',
sendNow: 'Send Now'
}
};
+1 -1
View File
@@ -1,7 +1,7 @@
const storeName = localStorage.getItem('StoreName') || 'WebSoft Inc';
/* 主框架 */
export default {
system: '小程序开发',
system: '小程序商城',
home: '主页',
header: {
profile: '个人资料',
+32 -1
View File
@@ -10,5 +10,36 @@ export default {
loading: '登录中',
oldPassword: '旧密码',
newPassword: '新密码',
confirm: '确认密码'
confirm: '确认密码',
forgetPage: {
title: '忘记密码',
subTitle: '请输入账号信息以重置密码',
tabEmail: '邮箱找回',
tabPhone: '手机找回',
email: '邮箱',
phone: '手机号',
emailCode: '邮箱验证码',
code: '短信验证码',
getCode: '获取验证码',
sentEmail: '验证码已发送,请查收邮箱',
sentPhone: '验证码已发送',
newPassword: '新密码',
confirmPassword: '确认新密码',
reset: '重置密码',
backLogin: '返回登录',
success: '密码重置成功,请使用新密码登录',
emailPlaceholder: '请输入注册邮箱',
phonePlaceholder: '请输入注册手机号',
codePlaceholder: '请输入验证码',
passwordPlaceholder: '至少 8 位,需包含字母和数字',
confirmPlaceholder: '请再次输入新密码',
newPasswordRule: '新密码至少 8 位,需包含字母和数字',
confirmRule: '两次输入的密码不一致',
emailRule: '请输入正确的邮箱地址',
phoneRule: '请输入正确的手机号',
codeRule: '请输入验证码',
captchaTitle: '发送验证码',
captchaPlaceholder: '请输入图形验证码',
sendNow: '立即发送'
}
};
+4
View File
@@ -1,7 +1,11 @@
<template>
<span>{{ item.meta.title }}</span>
<div v-if="item.meta && item.meta.badge" class="ele-menu-badge">
<!-- dot 模式纯红点不显示数字用于新订单提醒等 -->
<a-badge v-if="item.meta.badge === 'dot'" dot />
<!-- 数字徽章模式 -->
<a-badge
v-else
:count="item.meta.badge"
:number-style="{ background: item.meta.badgeColor as string }"
/>
+6
View File
@@ -100,6 +100,7 @@
import HeaderTools from './components/header-tools.vue';
import PageFooter from './components/page-footer.vue';
import MenuTitle from './components/menu-title.vue';
import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
import {
HIDE_SIDEBARS,
HIDE_FOOTERS,
@@ -127,6 +128,11 @@
const { t, locale } = useI18n();
const userStore = useUserStore();
//
// + /
const { start: startOrderNotify } = useOrderNotify();
startOrderNotify();
//
if (localStorage.getItem('Reload')) {
window.location.reload();
+5 -5
View File
@@ -50,11 +50,11 @@ export const routes = [
component: () => import('@/views/passport/merchant/success.vue'),
meta: { title: '申请提交成功' }
},
// {
// path: '/forget',
// component: () => import('@/views/passport/forget/index.vue'),
// meta: { title: '忘记密码' }
// },
{
path: '/forget',
component: () => import('@/views/passport/forget/index.vue'),
meta: { title: '忘记密码' }
},
// {
// path: '/wx-work-login',
// component: () => import('@/views/passport/wx-work/index.vue'),
+375
View File
@@ -0,0 +1,375 @@
/**
* store
* websopy db_websopy.app_subscription
* https://websopy-api.websoft.top/api
* com.gxwebsoft.app.controller.AppSubscriptionController
*/
import { defineStore } from 'pinia';
import {
pageMySubscriptions,
getSubscriptionDetail,
getSubscriptionDetailByNo,
checkSubscriptionStatus,
checkPurchased,
getBalance,
subscribe,
generatePayQrcode,
paySubscription,
mpPrepay,
mpConfirm,
renewSubscription,
cancelSubscription,
toggleEnable
} from '@/api/app/appSubscription';
import type {
AppSubscription,
AppSubscriptionQueryParam,
SubscribeParam,
SubscribeResult,
GeneratePayQrcodeParam,
GeneratePayQrcodeResult,
CheckStatusResult,
PayResult,
WechatNativePayResult,
MpPrepayResult
} from '@/api/app/appSubscription/model';
import type { PageResult } from '@/api';
export interface AppSubscriptionState {
// 我的订阅列表
subscriptionList: AppSubscription[];
// 总数量
total: number;
// 当前查看的订阅详情
currentSubscription: AppSubscription | null;
// 用户余额
balance: number;
// 加载状态
loading: boolean;
// 最后更新时间
lastUpdateTime: number | null;
// 缓存有效期(毫秒)
cacheExpiry: number;
}
export const useAppSubscriptionStore = defineStore('appSubscription', {
state: (): AppSubscriptionState => ({
subscriptionList: [],
total: 0,
currentSubscription: null,
balance: 0,
loading: false,
lastUpdateTime: null,
// 默认缓存5分钟(订阅涉及支付状态,不宜过长)
cacheExpiry: 5 * 60 * 1000
}),
getters: {
/**
*
*/
activeSubscriptions: (state): AppSubscription[] => {
return state.subscriptionList.filter((s) => s.status === 'active');
},
/**
*
*/
pendingSubscriptions: (state): AppSubscription[] => {
return state.subscriptionList.filter((s) => s.status === 'pending');
},
/**
*
*/
expiredSubscriptions: (state): AppSubscription[] => {
return state.subscriptionList.filter((s) => s.status === 'expired');
},
/**
*
*/
isCacheValid: (state): boolean => {
if (!state.lastUpdateTime) return false;
const now = Date.now();
return now - state.lastUpdateTime < state.cacheExpiry;
}
},
actions: {
// ============================================================
// 查询类
// ============================================================
/**
*
* @param params page/limit/status
* @param forceRefresh status true
*/
async fetchMySubscriptions(
params: AppSubscriptionQueryParam = { page: 1, limit: 10 },
forceRefresh = false
): Promise<PageResult<AppSubscription>> {
// 缓存有效且不强制刷新,直接返回缓存列表
if (!forceRefresh && this.isCacheValid && this.subscriptionList.length > 0) {
return { list: this.subscriptionList, count: this.total };
}
this.loading = true;
try {
const data = await pageMySubscriptions(params);
this.subscriptionList = data.list || [];
this.total = data.count || 0;
this.lastUpdateTime = Date.now();
return data;
} catch (error) {
console.error('获取我的订阅列表失败:', error);
throw error;
} finally {
this.loading = false;
}
},
/**
* ID
*/
async fetchDetail(id: number): Promise<AppSubscription> {
try {
const data = await getSubscriptionDetail(id);
this.currentSubscription = data;
return data;
} catch (error) {
console.error('获取订阅详情失败:', error);
throw error;
}
},
/**
*
*/
async fetchDetailByNo(subscriptionNo: string): Promise<AppSubscription> {
try {
const data = await getSubscriptionDetailByNo(subscriptionNo);
this.currentSubscription = data;
return data;
} catch (error) {
console.error('根据订阅编号查询详情失败:', error);
throw error;
}
},
/**
*
*/
async checkStatus(subscriptionNo: string): Promise<CheckStatusResult> {
return await checkSubscriptionStatus(subscriptionNo);
},
/**
*
*/
async checkPurchased(productId: number): Promise<boolean> {
return await checkPurchased(productId);
},
/**
*
*/
async fetchBalance(): Promise<number> {
try {
const data = await getBalance();
this.balance = data.balance;
return data.balance;
} catch (error) {
console.error('获取用户余额失败:', error);
throw error;
}
},
// ============================================================
// 订阅与支付操作
// ============================================================
/**
*
*
*/
async subscribe(data: SubscribeParam): Promise<SubscribeResult> {
try {
const result = await subscribe(data);
// 订阅状态变化,失效缓存
this.invalidateCache();
return result;
} catch (error) {
console.error('创建订阅失败:', error);
throw error;
}
},
/**
*
*/
async generatePayQrcode(
data: GeneratePayQrcodeParam
): Promise<GeneratePayQrcodeResult> {
try {
const result = await generatePayQrcode(data);
// 已创建订阅记录,失效缓存
this.invalidateCache();
return result;
} catch (error) {
console.error('生成支付小程序码失败:', error);
throw error;
}
},
/**
*
* @param id ID
* @param method balance- / wechat-
* @param envVersion 使develop/trial/release
*/
async pay(
id: number,
method: 'balance' | 'wechat' = 'wechat',
envVersion?: string
): Promise<PayResult | WechatNativePayResult> {
try {
const result = await paySubscription(id, method, envVersion);
// 支付成功后余额/订阅状态变化,失效缓存并刷新余额
this.invalidateCache();
if (method === 'balance') {
const payResult = result as PayResult;
if (typeof payResult.balance === 'number') {
this.balance = payResult.balance;
}
}
return result;
} catch (error) {
console.error('发起支付失败:', error);
throw error;
}
},
/**
* JSAPI
*/
async mpPrepay(id: number, openid: string): Promise<MpPrepayResult> {
try {
return await mpPrepay(id, { openid });
} catch (error) {
console.error('小程序预支付失败:', error);
throw error;
}
},
/**
*
*/
async mpConfirm(
subscriptionNo: string,
transactionId?: string
): Promise<PayResult> {
try {
const result = await mpConfirm(subscriptionNo, { transactionId });
// 支付确认后订阅状态变化,失效缓存
this.invalidateCache();
return result;
} catch (error) {
console.error('小程序支付确认失败:', error);
throw error;
}
},
// ============================================================
// 订阅管理
// ============================================================
/**
* /renew-pay
* @param id ID
* @param period month/year
* @param method balance/wechat
* @param envVersion 使
*/
async renew(
id: number,
period: 'month' | 'year' = 'month',
method: 'balance' | 'wechat' = 'wechat',
envVersion?: string
): Promise<PayResult | WechatNativePayResult> {
try {
const result = await renewSubscription(id, period, method, envVersion);
this.invalidateCache();
return result;
} catch (error) {
console.error('续费失败:', error);
throw error;
}
},
/**
* 退/
*/
async cancel(id: number): Promise<string> {
try {
const msg = await cancelSubscription(id);
this.invalidateCache();
return msg;
} catch (error) {
console.error('退订失败:', error);
throw error;
}
},
/**
* /
*/
async toggleEnable(id: number, enabled: boolean): Promise<string> {
try {
const msg = await toggleEnable(id, enabled);
this.invalidateCache();
return msg;
} catch (error) {
console.error('启用/禁用失败:', error);
throw error;
}
},
// ============================================================
// 缓存控制
// ============================================================
/**
*
* //
*/
invalidateCache() {
this.lastUpdateTime = null;
},
/**
*
*/
clearCache() {
this.subscriptionList = [];
this.total = 0;
this.currentSubscription = null;
this.lastUpdateTime = null;
},
/**
*
*/
async refresh() {
return await this.fetchMySubscriptions({ page: 1, limit: 10 }, true);
},
/**
*
*/
setCacheExpiry(expiry: number) {
this.cacheExpiry = expiry;
}
}
});
+56 -264
View File
@@ -1,21 +1,26 @@
/**
* store
* ///
* /shop/shop-order/stats/overview cms_statistics
*/
import { defineStore } from 'pinia';
import dayjs from 'dayjs';
import { pageUsers } from '@/api/system/user';
import { pageShopOrder, shopOrderTotal, listShopOrder } from '@/api/shop/shopOrder';
import {
addCmsStatistics,
listCmsStatistics,
updateCmsStatistics
} from '@/api/cms/cmsStatistics';
import { CmsStatistics } from '@/api/cms/cmsStatistics/model';
import { safeNumber, hasValidId } from '@/utils/type-guards';
import { pageShopOrder, shopOrderTotal } from '@/api/shop/shopOrder';
import { getShopOrderStatsOverview } from '@/api/shop/shopOrderStats';
import { safeNumber } from '@/utils/type-guards';
export interface StatisticsState {
// 统计数据
statistics: CmsStatistics | null;
statistics: {
userCount: number;
orderCount: number;
totalSales: number;
todaySales: number;
monthSales: number;
todayOrders: number;
todayUsers: number;
} | null;
// 加载状态
loading: boolean;
// 最后更新时间
@@ -40,65 +45,14 @@ export const useStatisticsStore = defineStore('statistics', {
}),
getters: {
/**
*
*/
userCount: (state): number => {
return safeNumber(state.statistics?.userCount);
},
/**
*
*/
orderCount: (state): number => {
return safeNumber(state.statistics?.orderCount);
},
/**
*
*/
totalSales: (state): number => {
return safeNumber(state.statistics?.totalSales);
},
/**
*
*/
todaySales: (state): number => {
return safeNumber(state.statistics?.todaySales);
},
/**
*
*/
monthSales: (state): number => {
return safeNumber(state.statistics?.monthSales);
},
/**
*
*/
todayOrders: (state): number => {
return safeNumber(state.statistics?.todayOrders);
},
/**
*
*/
todayUsers: (state): number => {
return safeNumber(state.statistics?.todayUsers);
},
/**
* 使
*/
couponUsedCount: (state): number => {
return safeNumber(state.couponUsedCount);
},
/**
*
*/
userCount: (state): number => safeNumber(state.statistics?.userCount),
orderCount: (state): number => safeNumber(state.statistics?.orderCount),
totalSales: (state): number => safeNumber(state.statistics?.totalSales),
todaySales: (state): number => safeNumber(state.statistics?.todaySales),
monthSales: (state): number => safeNumber(state.statistics?.monthSales),
todayOrders: (state): number => safeNumber(state.statistics?.todayOrders),
todayUsers: (state): number => safeNumber(state.statistics?.todayUsers),
safeCouponUsedCount: (state): number => safeNumber(state.couponUsedCount),
isCacheValid: (state): boolean => {
if (!state.lastUpdateTime) return false;
const now = Date.now();
@@ -107,40 +61,35 @@ export const useStatisticsStore = defineStore('statistics', {
},
actions: {
/**
*
* @param forceRefresh
*/
async fetchStatistics(forceRefresh = false) {
// 如果缓存有效且不强制刷新,直接返回缓存数据
if (!forceRefresh && this.isCacheValid && this.statistics) {
return this.statistics;
}
this.loading = true;
try {
// 并行获取各种统计数据,使用Promise.allSettled确保部分失败不影响整体
const [usersResult, ordersResult, totalResult, statisticsResult] =
// 今日区间(与后端 dayjs startOf/endOf('day') 对齐)
const todayStart = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
const todayEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
// 并行获取:累计用户数 / 累计订单数 / 累计实收 / 今日概览(后端聚合)
const [usersResult, ordersResult, totalResult, overviewResult] =
await Promise.allSettled([
pageUsers({ page: 1, limit: 1 }),
pageShopOrder({ page: 1, limit: 1 }),
shopOrderTotal(),
listCmsStatistics({})
getShopOrderStatsOverview({ start: todayStart, end: todayEnd })
]);
// 安全提取结果
const users =
usersResult.status === 'fulfilled' ? usersResult.value : null;
const orders =
ordersResult.status === 'fulfilled' ? ordersResult.value : null;
const total =
totalResult.status === 'fulfilled' ? totalResult.value : null;
const statisticsData =
statisticsResult.status === 'fulfilled'
? statisticsResult.value
: null;
const overview =
overviewResult.status === 'fulfilled' ? overviewResult.value : null;
// 记录失败的API调用
if (usersResult.status === 'rejected') {
console.error('❌ 用户API调用失败:', usersResult.reason);
}
@@ -150,178 +99,41 @@ export const useStatisticsStore = defineStore('statistics', {
if (totalResult.status === 'rejected') {
console.error('❌ 订单总额API调用失败:', totalResult.reason);
}
if (statisticsResult.status === 'rejected') {
console.error('❌ 统计数据API调用失败:', statisticsResult.reason);
if (overviewResult.status === 'rejected') {
console.error('❌ 今日概览API调用失败:', overviewResult.reason);
}
// 添加调试日志
console.log('🔍 统计数据获取结果:', {
users: users,
orders: orders,
total: total,
statisticsData: statisticsData
});
const userCount =
users && typeof users === 'object' && 'count' in users
? safeNumber((users as any).count)
: 0;
const orderCount =
orders && typeof orders === 'object' && 'count' in orders
? safeNumber((orders as any).count)
: 0;
const totalSales = safeNumber(total);
let statistics: CmsStatistics;
// 今日数据走后端聚合接口(实收 + 总数 + 新增会员 + 用券数)
const todaySales = overview ? safeNumber(overview.paidSales) : 0;
const todayOrders = overview ? safeNumber(overview.orderCount) : 0;
const todayUsers = overview ? safeNumber(overview.newUserCount) : 0;
const couponUsedCount = overview
? safeNumber(overview.couponUsedCount)
: 0;
// 安全获取用户数量,添加更详细的验证
const userCount = (() => {
if (!users) {
console.warn('⚠️ 用户API返回空数据');
return 0;
}
if (typeof users === 'object' && 'count' in users) {
const count = users.count;
console.log('✅ 用户数量:', count);
return safeNumber(count);
}
console.warn('⚠️ 用户API返回数据格式不正确:', users);
return 0;
})();
// 安全获取订单数量
const orderCount = (() => {
if (!orders) {
console.warn('⚠️ 订单API返回空数据');
return 0;
}
if (typeof orders === 'object' && 'count' in orders) {
const count = orders.count;
console.log('✅ 订单数量:', count);
return safeNumber(count);
}
console.warn('⚠️ 订单API返回数据格式不正确:', orders);
return 0;
})();
// 实时计算今日数据(不依赖可能未更新的统计表)
const todayStart = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
const todayEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
// 安全获取今日订单列表、销售额和优惠券使用量
let todayOrders = 0;
let todaySales = 0;
let couponUsedCount = 0;
try {
const todayOrderList = await listShopOrder({
createTimeStart: todayStart,
createTimeEnd: todayEnd
});
if (Array.isArray(todayOrderList)) {
todayOrders = todayOrderList.length;
todaySales = todayOrderList.reduce((acc, order) => {
return acc + (order.payStatus ? safeNumber(order.payPrice) : 0);
}, 0);
couponUsedCount = todayOrderList.filter((order) => {
const couponType = order.couponType;
return couponType !== undefined && couponType !== null && couponType !== 0;
}).length;
}
} catch (e) {
console.warn('⚠️ 获取今日订单列表失败:', e);
}
// 安全获取今日新增用户
let todayUsers = 0;
try {
const todayUsersResult = await pageUsers({
page: 1,
limit: 1,
createTimeStart: todayStart,
createTimeEnd: todayEnd
});
if (todayUsersResult && typeof todayUsersResult === 'object' && 'count' in todayUsersResult) {
todayUsers = safeNumber(todayUsersResult.count);
}
} catch (e) {
console.warn('⚠️ 获取今日新增用户失败:', e);
}
const totalSales = (() => {
if (!total) {
console.warn('⚠️ 订单总额API返回空数据');
return 0;
}
if (Array.isArray(total)) {
// 如果是数组,计算总金额
const sum = total.reduce((acc, order) => {
const amount = order.payPrice || order.totalPrice || 0;
return acc + safeNumber(amount);
}, 0);
console.log('✅ 总销售额(数组计算):', sum);
return sum;
}
const amount = safeNumber(total);
console.log('✅ 总销售额(直接值):', amount);
return amount;
})();
if (statisticsData && statisticsData.length > 0) {
// 更新现有统计数据
const existingStatistics = statisticsData[0];
// 确保数据存在且有有效的 ID
if (hasValidId(existingStatistics)) {
const updateData: Partial<CmsStatistics> = {
id: existingStatistics.id,
userCount: userCount,
orderCount: orderCount,
totalSales: totalSales,
todaySales: todaySales,
todayOrders: todayOrders,
todayUsers: todayUsers
};
// 异步更新数据库
setTimeout(() => {
updateCmsStatistics(updateData).catch((error) => {
console.error('更新统计数据失败:', error);
});
}, 1000);
// 更新本地数据
statistics = {
...existingStatistics,
...updateData,
this.statistics = {
userCount,
orderCount,
totalSales,
todaySales,
monthSales: todaySales,
todayOrders,
todayUsers
};
} else {
// 如果现有数据无效,使用基础数据
statistics = {
userCount: userCount,
orderCount: orderCount,
totalSales: totalSales,
todaySales: todaySales,
todayOrders: todayOrders,
todayUsers: todayUsers
};
}
} else {
// 创建新的统计数据
statistics = {
userCount: userCount,
orderCount: orderCount,
totalSales: totalSales,
todaySales: todaySales,
todayOrders: todayOrders,
todayUsers: todayUsers
};
// 异步保存到数据库
setTimeout(() => {
addCmsStatistics(statistics).catch((error) => {
console.error('保存统计数据失败:', error);
});
}, 1000);
}
this.statistics = statistics;
this.couponUsedCount = couponUsedCount;
this.lastUpdateTime = Date.now();
return statistics;
return this.statistics;
} catch (error) {
console.error('获取统计数据失败:', error);
throw error;
@@ -330,44 +142,27 @@ export const useStatisticsStore = defineStore('statistics', {
}
},
/**
*
*/
updateStatistics(statistics: Partial<CmsStatistics>) {
updateStatistics(statistics: Partial<StatisticsState['statistics']>) {
if (this.statistics) {
this.statistics = { ...this.statistics, ...statistics };
this.lastUpdateTime = Date.now();
}
},
/**
*
*/
clearCache() {
this.statistics = null;
this.lastUpdateTime = null;
},
/**
*
*/
async forceRefresh() {
console.log('🔄 强制刷新统计数据...');
this.clearCache();
return await this.fetchStatistics(true);
},
/**
*
*/
setCacheExpiry(expiry: number) {
this.cacheExpiry = expiry;
},
/**
*
* @param interval 5
*/
startAutoRefresh(interval = 5 * 60 * 1000) {
this.stopAutoRefresh();
this.refreshTimer = window.setInterval(() => {
@@ -375,9 +170,6 @@ export const useStatisticsStore = defineStore('statistics', {
}, interval);
},
/**
*
*/
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
+49 -7
View File
@@ -41,6 +41,26 @@ export function ensureFullUrl(url: string): string {
return `${OSS_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`;
}
/**
* URL x-oss-process query
*
* OSS `x-oss-process=image/...` query
* query
*
* @param url URL
* @returns x-oss-process URL
*/
function removeOssProcess(url: string): string {
const qIndex = url.indexOf('?');
if (qIndex === -1) return url;
const base = url.slice(0, qIndex);
const params = url
.slice(qIndex + 1)
.split('&')
.filter((p) => !p.startsWith('x-oss-process='));
return params.length ? `${base}?${params.join('&')}` : base;
}
/**
* OSS URL
*
@@ -49,28 +69,50 @@ export function ensureFullUrl(url: string): string {
*
* URL
*
* width/quality URL x-oss-process
*
*
* @param url URL
* @param options
* @param options width/quality 使
* @returns URL
*/
// 缓存已计算的压缩 URL,避免列表重渲染时(勾选、hover、滚动)重复拼接字符串
const compressedUrlCache = new Map<string, string>();
export function getCompressedImageUrl(
url: string,
options?: ImageCompressOptions
): string {
if (!url) return '';
// 命中缓存直接返回,省去重复计算
const cacheKey = options ? `${url}|${JSON.stringify(options)}` : url;
const cached = compressedUrlCache.get(cacheKey);
if (cached !== undefined) return cached;
// 先补全为完整 URL
const fullUrl = ensureFullUrl(url);
if (!fullUrl) return '';
if (!fullUrl) {
compressedUrlCache.set(cacheKey, '');
return '';
}
const { width, quality, enabled } = { ...DEFAULT_OPTIONS, ...options };
// 未启用压缩,原样返回
if (!enabled) return fullUrl;
if (!enabled) {
compressedUrlCache.set(cacheKey, fullUrl);
return fullUrl;
}
// 已包含 OSS 处理参数,避免重复拼接
if (fullUrl.includes('x-oss-process')) return fullUrl;
// 优先使用调用方指定的 width/quality
// 先剥离 URL 自带的 x-oss-process,再按本函数参数重新拼接,确保以调用方为准。
const baseUrl = removeOssProcess(fullUrl);
// 已有其他 query 参数用 & 拼接,否则用 ?
const separator = fullUrl.includes('?') ? '&' : '?';
const separator = baseUrl.includes('?') ? '&' : '?';
return `${fullUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
const result = `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
compressedUrlCache.set(cacheKey, result);
return result;
}
+5
View File
@@ -47,6 +47,11 @@ service.interceptors.request.use(
}
// 获取租户ID
if (config.headers) {
// 登录前场景(找回密码等)由调用方显式设置 X-Tenant-Skip
// 此时不做任何租户/企业/商户/域名头注入,避免串租户
if (config.headers['X-Tenant-Skip']) {
return config;
}
// 附加企业ID
const companyId = localStorage.getItem('CompanyId');
if (companyId) {
+4
View File
@@ -36,6 +36,10 @@ export function getPayType(index?: number): any {
{
value: 8,
label: '货到付款'
},
{
value: 9,
label: '线下付款'
}
];
if (index != null) {
+2 -6
View File
@@ -333,16 +333,12 @@
const chooseFile = (data: FileRecord) => {
images.value.push({
uid: data.id,
url:
data.downloadUrl +
'?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90',
url: data.downloadUrl,
status: 'done',
title: '', //
path: '' //
});
form.images =
data.downloadUrl +
'?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90';
form.images = data.downloadUrl;
};
const onDeleteItem = (index: number) => {
+6 -1
View File
@@ -47,7 +47,11 @@
v-for="(item, index) in record.imageList"
:key="index"
>
<a-image :src="item.url" :width="80" />
<a-image
:src="getCompressedImageUrl(item.url, { width: 160, quality: 90 })"
:preview="{ src: item.url }"
:width="80"
/>
</template>
<template v-if="record.type == 4">
{{ record.comments }}
@@ -113,6 +117,7 @@
import { listCmsNavigation } from '@/api/cms/cmsNavigation';
import { getPageTitle } from '@/utils/common';
import { getTenantId } from '@/utils/domain';
import { getCompressedImageUrl } from '@/utils/image';
//
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);

Some files were not shown because too many files have changed in this diff Show More