fix: resolve admin acceptance defects
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
# Olons 功能测试执行报告(2026-10-16)
|
||||
|
||||
## 1. 执行说明
|
||||
|
||||
- 测试依据:`Olons全功能测试用例.md`(用户补充后的 548 条版本)
|
||||
- 执行重点:优先从页面真实操作验证,其次执行构建与后端自动化检查
|
||||
- C 端环境:`https://olns-web.118im.com/admin/`
|
||||
- 管理后台环境:`https://olons-admin.118im.com/admin`
|
||||
- 执行策略:第一轮仅做不破坏现有业务数据的页面检查;2026-10-16 用户随后授权使用专用测试数据执行新增、删除、审核、资金及配置变更测试
|
||||
- 状态含义:`通过`、`部分通过`、`失败`、`阻塞`、`未执行`
|
||||
|
||||
## 2. 第一轮结论
|
||||
|
||||
第一轮已完成 C 端主要页面的真实加载、数据回显及主要入口检查,并执行了两套前端生产构建和后台 Go 测试。当前发现 1 个明确的页面功能缺陷、1 个测试工程问题;管理后台逐按钮测试因执行机器锁屏暂时中断,锁屏问题不计入产品缺陷。
|
||||
|
||||
| 类别 | 结果 |
|
||||
|---|---|
|
||||
| C 端生产构建 | 通过 |
|
||||
| 管理后台生产构建 | 通过(需绕过 pnpm 的自动安装检查,直接使用现有依赖构建) |
|
||||
| 后台 Go 全量测试 | 失败:Casbin、TCP、存储器用例依赖运行环境或测试数据,出现 panic |
|
||||
| C 端页面冒烟 | 首页、领取、记录、商城、储入、取出、我的、个人信息、客服中心均可加载 |
|
||||
| 明确产品缺陷 | 1 个:已有收款账户未在“绑定信息”页回显 |
|
||||
| 后台页面逐按钮 | 阻塞:执行机器进入锁屏,待解锁续测 |
|
||||
|
||||
## 3. C 端页面执行记录
|
||||
|
||||
| 页面/关联用例 | 状态 | 实际结果 |
|
||||
|---|---|---|
|
||||
| 首页 `/app/home`;UI-C-HOME-001、UI-C-HOME-003、UI-C-HOME-004 | 部分通过 | 页面正常显示会员信息、余额、领取/储入/取出/商城四个入口、排行、平台介绍、规则说明、公司资质、代理合作及底部导航;本轮未执行会产生业务状态变化的领取动作 |
|
||||
| 领取 `/app/claim`;UI-C-CLAIM-001、UI-C-CLAIM-005 | 通过 | 页面正常显示总资产、昨日佣金、今日佣金、今日领取数;当前会员存在未完成订单,页面正确提示“存在未完成订单”并提供“查看任务”入口 |
|
||||
| 订单 `/app/orders`;UI-C-ORDER-001、UI-C-ORDER-002 | 部分通过 | 全部、待处理、已完成标签及订单卡片正常显示;本轮未对订单执行提交或取消 |
|
||||
| 商城 `/app/mall`;UI-C-MALL-001、UI-C-MALL-002 | 部分通过 | 搜索、推荐、限时区域及商品卡片正常显示;“验收临时商品-商品页已编辑”可见 |
|
||||
| 储入 `/app/recharge`;UI-C-RECHARGE-001、UI-C-RECHARGE-002 | 部分通过 | 等级说明、微信/支付宝/银行卡/人工审核渠道、金额、姓名及提交入口正常显示;未执行真实提交 |
|
||||
| 取出 `/app/profile/withdrawal`;UI-C-WITHDRAW-001、UI-C-WITHDRAW-002 | 部分通过 | 可用余额、全部提现、已绑定账户 `icbc 6655******4555` 及申请提现入口正常显示;未执行真实提现 |
|
||||
| 我的 `/app/profile`;UI-C-PROFILE-001、UI-C-PROFILE-002 | 通过 | 会员 potter、邀请码 100005、余额及个人中心入口正常显示 |
|
||||
| 个人信息 `/app/profile/info`;UI-C-FORM-001、UI-C-FORM-002 | 通过 | 头像、手机号、取出信息、密码管理、支付密码管理入口正常显示 |
|
||||
| 绑定信息 `/app/profile/bank-card`;UI-C-FORM-003、UI-C-FORM-010、C-FIN-039 | 失败 | 该会员在取出页已有银行卡,但绑定信息页仍显示空白新增表单,没有回显本人原始账户信息 |
|
||||
| 客服中心 `/app/support`;UI-C-SUPPORT-001 | 通过 | 客服依依、客服琪琪、客服琳琳均显示;在线/离线状态、工作时间和联系电话正常显示 |
|
||||
|
||||
## 4. 缺陷记录
|
||||
|
||||
### DEF-C-001 已绑定收款账户未在“绑定信息”页回显
|
||||
|
||||
| 项目 | 内容 |
|
||||
|---|---|
|
||||
| 严重级别 | P1 |
|
||||
| 关联用例 | UI-C-FORM-003、UI-C-FORM-010、UI-C-FORM-011、C-FIN-039 |
|
||||
| 前置条件 | 会员已登录,且已有收款账户 |
|
||||
| 复现步骤 | 1. 打开“我的 → 取出”,确认页面显示 `icbc 6655******4555`;2. 打开“我的 → 个人信息 → 取出信息/绑定信息” |
|
||||
| 预期结果 | 页面显示本人姓名、身份证号、银行名、完整卡号,以及已上传的微信/支付宝收款码;允许按业务规则编辑或继续新增 |
|
||||
| 实际结果 | 页面只显示空白的新增绑定表单,已有账户信息未回显 |
|
||||
| 影响 | 用户无法从个人中心核对已有收款资料,容易重复绑定或误判资料丢失 |
|
||||
| 建议检查 | 检查页面是否调用 `/v1/payment-accounts/details`;确认接口返回账户列表后是否被表单初始化逻辑覆盖;多账户场景需逐条展示并保持收款码对应关系 |
|
||||
|
||||
补充诊断:
|
||||
|
||||
- 已核对线上 C 端静态资源,当前发布包包含 `/v1/payment-accounts/details`、`已绑定的取出信息` 和 `添加银行卡`,不是前端漏发版。
|
||||
- 已只读核对测试库:会员 `potter`(member_id=9、tenant_id=0)确有 1 条未删除的 `icbc` 收款账户,脱敏卡号为 `6655******4555`,密文字段及微信/支付宝附件 ID 均非空。
|
||||
- 本地代码的详情接口会解密敏感字段;只要服务端 finance key 未配置、与写入时密钥不一致,或详情接口返回异常,页面就会保留空数组并显示新增表单。下一步应抓取该登录会话下 `/v1/payment-accounts/details` 的真实响应,重点检查 `OLONS_SERVER_FINANCE_MASTER_KEY` 与该账户写入时使用的密钥是否一致。
|
||||
|
||||
## 5. 构建与自动化检查
|
||||
|
||||
### 5.1 C 端前端构建
|
||||
|
||||
- 目录:`olons_server/web`
|
||||
- 命令:`pnpm run build`
|
||||
- 结果:通过
|
||||
- 构建结果:4186 个模块完成转换,生产包成功生成
|
||||
|
||||
### 5.2 管理后台前端构建
|
||||
|
||||
- 目录:`olons_admin/web`
|
||||
- 常规 `pnpm run build`:被 pnpm 的依赖脚本安全检查阻断,提示 `ERR_PNPM_IGNORED_BUILDS`
|
||||
- 使用已有依赖直接执行 Vite 与 HotGo postBuild:通过
|
||||
- 构建结果:6728 个模块完成转换,`dist/app.config.js` 正常生成
|
||||
- 说明:这是本机 pnpm 依赖脚本策略问题,不是业务代码编译失败;CI 应固定 pnpm 版本及 `onlyBuiltDependencies/approve-builds` 配置,避免发布时临时阻断
|
||||
|
||||
### 5.3 后台 Go 测试
|
||||
|
||||
- 目录:`olons_admin/server`
|
||||
- 命令:`go test ./...`
|
||||
- 结果:失败
|
||||
- 失败 1:`internal/library/casbin.TestNew`,错误为 `casbin model file does not exist:manifest/config/casbin.conf`
|
||||
- 失败 2:`internal/library/network/tcp_test.TestSendMsg`,依赖的 `127.0.0.1:8002` 未启动;连接失败后仍调用空连接的 `Send`,触发空指针 panic
|
||||
- 失败 3:`internal/library/storager.TestMergePartFile`,测试数据/参数无效后仍继续使用空对象,触发空指针 panic
|
||||
- 判断:上述均属于测试工程对当前目录、外部服务或本地文件的隐式依赖,同时缺少失败后的安全返回;本轮 `internal/logic/sys` 等核心业务包测试通过
|
||||
- 建议:Casbin 测试使用可确定的项目根路径或 `testdata`;TCP 测试自建临时监听服务;存储测试自建临时文件;所有前置失败后立即 `t.Fatal` 或返回,避免二次 panic
|
||||
|
||||
### 5.4 线上可用性与未登录鉴权
|
||||
|
||||
| 检查项 | 实际结果 | 结论 |
|
||||
|---|---|---|
|
||||
| `GET https://olns-web.118im.com/api/health` | HTTP 200 | 通过 |
|
||||
| 未登录访问 `GET /v1/me` | HTTP 401 | 通过 |
|
||||
| 未登录访问 `GET /v1/payment-accounts/details` | HTTP 401 | 通过 |
|
||||
| 管理后台入口 | HTTP 200 | 通过 |
|
||||
|
||||
## 6. 阻塞与待执行项
|
||||
|
||||
1. 管理后台 URL 地址池、会员登录日志及其他菜单逐按钮验收:执行机器锁屏,待解锁后续测。
|
||||
2. 新增、编辑、删除、发布/下架、审核、充值、提现、任务提交、客服发消息等写操作:需要专用测试数据并确认允许产生或清理测试记录后执行。
|
||||
3. URL 地址池 Excel 合法、重复、错误行导入:需要使用用例中的 XLS-URL 文件执行,会产生数据库写入,暂未提交。
|
||||
4. 多账户详情、收款码图片一致性、敏感信息展示权限:需在专用测试会员上执行,避免暴露或修改真实会员资料。
|
||||
|
||||
## 7. 下一轮计划
|
||||
|
||||
1. 解锁后从后台菜单真实点击进入 URL 地址池,依次验证查询、重置、新增/编辑弹窗、详情、启用/失效、复制链接、Excel 导入弹窗及无删除按钮。
|
||||
2. 验证会员登录日志查询、分页、详情和导出入口。
|
||||
3. 按菜单顺序完成交易、商品、任务、组织、权限、系统应用、系统设置页面的所有可逆操作。
|
||||
4. 建立专用测试会员、商品、订单和 URL 地址数据,再执行会改变业务状态的 P0/P1 用例。
|
||||
5. 修复 DEF-C-001 后,执行绑定信息与提现账户关联回归。
|
||||
|
||||
## 8. 第二轮后台页面执行记录(解锁后)
|
||||
|
||||
### 8.1 URL 地址池与会员登录日志
|
||||
|
||||
| 页面/关联用例 | 状态 | 实际结果 |
|
||||
|---|---|---|
|
||||
| URL 地址池;UI-A-URL-001、002 | 通过 | 汇总卡、筛选、展开/收起、查询、重置、分页和空状态正常;重置可清空条件 |
|
||||
| URL 地址池新增;UI-A-URL-003 | 部分通过 | 新增弹窗包含地址名称、注册入口 URL、状态、备注及保存/取消;未提交写库 |
|
||||
| URL 地址池详情/启停/复制;UI-A-URL-004、005 | 阻塞 | 当前列表 0 条,无可操作数据 |
|
||||
| URL Excel 导入;UI-A-URL-006 | 部分失败 | 格式说明、关闭入口、`.xlsx/.xlsm`、5000 条限制、重复跳过和错误行说明完整,但弹窗没有“取消”按钮 |
|
||||
| 会员登录日志;UI-A-MLOGIN 系列 | 部分通过 | 会员、方式、状态、域名、IP、时间筛选齐全;查询、重置、展开/收起和导出均可用;当前列表 0 条,详情无法验证 |
|
||||
|
||||
### 8.2 交易管理
|
||||
|
||||
| 页面/关联用例 | 状态 | 实际结果 |
|
||||
|---|---|---|
|
||||
| 订单列表;UI-A-ORDER-001 | 部分失败 | 单号、用户名、手机号、状态、时间筛选可用,但缺少用例要求的“派单方式”筛选 |
|
||||
| 订单详情;UI-A-ORDER-002、A-TRADE-002 | 失败 | 仅显示订单号、会员、商品/任务、等级、金额和规则快照;缺少时间线、资金流水等追溯信息 |
|
||||
| 匹配订单;UI-A-ORDER-003、A-TRADE-003 | 通过 | 弹窗可按商品名和价格筛选;发布商品可见,保时捷跑车、新手体验商品等均能查询;清理筛选和取消正常 |
|
||||
| 充值列表;UI-A-RC-001 | 部分失败 | 汇总卡、列表、通过/驳回入口正常;多条真实记录的“真实姓名”列为空,仅在备注中出现登录名 |
|
||||
| 充值通过/驳回;UI-A-RC-002、003 | 部分通过 | 通过弹窗包含实付金额、外部支付单号和审核说明;驳回弹窗包含原因与说明;取消无副作用,未执行最终审核 |
|
||||
| 提现列表;UI-A-WD-001 | 部分通过 | 金额、手续费、实到、状态和银行卡脱敏正常;当前 potter 记录的支付宝/微信收款码均显示“-”,需结合账户附件继续核对 |
|
||||
| 提现通过/驳回;UI-A-WD-002 | 通过 | 合法状态显示按钮,二次确认文案正确,取消无副作用;未执行最终审核 |
|
||||
| 客服管理;UI-A-CS-001、002 | 部分通过 | 查询、新增/编辑表单、IM 账号和备用联系方式完整;启停操作即时生效,测试后已恢复原状态;未执行删除 |
|
||||
|
||||
### 8.3 商品、任务和权限
|
||||
|
||||
| 页面/关联用例 | 状态 | 实际结果 |
|
||||
|---|---|---|
|
||||
| 商品列表;UI-A-PROD-001、006 | 通过 | 名称/分类筛选、添加、批量删除入口完整;编辑、发布/下架、推荐/不推荐、删除均可见且操作列未截断 |
|
||||
| 商品表单;UI-A-PROD-002 | 部分通过 | 名称、分类、店铺、价格、图片、详情字段完整;未保存写库 |
|
||||
| 任务列表;UI-A-TASK-001 | 失败 | 列表、状态操作和任务基本字段正常,但新增任务弹窗没有“商品”选择字段,与用例和任务关联设计不一致 |
|
||||
| 会员任务审核;UI-A-SUB-001、004 | 失败 | 列表“任务标题”全部为空;详情中的“任务”也为空,并且没有订单信息,不能完成会员—订单—任务追溯 |
|
||||
| 角色菜单权限;UI-A-ROLE-002、003 | 部分通过 | 弹窗明确限制为父角色权限子集;主页、组织管理、系统应用、系统设置等根节点均可勾选;勾选组织管理后“后台用户”自动选中,父子级联正常;未提交保存 |
|
||||
| 系统应用菜单;UI-A-APP-001 | 通过 | 日志管理、附件管理、个人中心入口均存在,个人中心已恢复可见 |
|
||||
|
||||
## 9. 第二轮新增缺陷
|
||||
|
||||
### DEF-A-001 URL 地址池 Excel 导入弹窗缺少取消按钮
|
||||
|
||||
- 严重级别:P2
|
||||
- 关联用例:UI-A-URL-006
|
||||
- 实际结果:弹窗仅提供右上角关闭,没有底部取消按钮。
|
||||
- 影响:与后台其他弹窗交互不一致,触屏或窄屏下关闭入口不够明显。
|
||||
|
||||
### DEF-A-002 订单详情缺少完整业务追溯信息
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-ORDER-002、A-TRADE-002、A-TRADE-030
|
||||
- 实际结果:详情只展示基础字段和 JSON 规则快照,没有订单状态时间线、付款/冻结/佣金流水、审核记录。
|
||||
- 影响:客服和财务无法从单一页面核对订单状态与资金变化。
|
||||
- 代码定位:`TradeOrderViewModel` 只在列表模型上补充 `taskId/productId/memberLevelId/ruleSnapshot`,`trade_order.go` 的 `View` 也只组装这些字段;前端详情弹窗没有请求或渲染资金流水、审批动作和状态时间线。
|
||||
|
||||
### DEF-A-003 多个后台交易页面直接显示完整手机号
|
||||
|
||||
> 处理结论(2026-10-16):产品方确认会员列表、详情及相关业务页面允许显示完整手机号,本项改判为“已接受设计”,不再作为缺陷,不实施手机号脱敏改造。
|
||||
|
||||
- 严重级别:P1
|
||||
- 涉及页面:会员列表/详情、订单列表/详情、充值列表、会员任务审核列表/详情
|
||||
- 实际结果:显示 `13311111111`、`15122421111` 等完整手机号。
|
||||
- 预期结果:默认脱敏;只有具备单独敏感信息权限的人员才能查看明文,且需留痕。
|
||||
- 代码定位:订单和任务审核查询直接选择 `hg_app_member.mobile`,前端表格直接绑定 `mobile`;当前链路没有脱敏 DTO,也没有敏感字段权限判断。
|
||||
- 补测结果:会员详情明确提供“授权查看明文”按钮,但未授权前手机号已经完整显示,说明当前授权入口只停留在 UI,未形成真正的数据隔离。
|
||||
|
||||
### DEF-A-004 订单列表缺少派单方式筛选
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-ORDER-001、A-TRADE-001
|
||||
- 实际结果:高级筛选只有用户名、手机号、订单状态和下单时间,没有自动/手动派单方式。
|
||||
- 代码定位:前端 `trade/order/model.ts` 的 `schemas` 和后端 `TradeOrderListInp` 均没有派单方式查询字段,属于前后端共同缺项。
|
||||
|
||||
### DEF-A-005 新增任务弹窗缺少商品字段
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-TASK-001
|
||||
- 实际结果:弹窗包含任务大厅、等级、佣金、数量、周期、封面和详情,但没有关联商品选择。
|
||||
- 影响:无法按设计建立任务与商品的明确关联。
|
||||
- 代码定位:`hg_task` 对应实体和 `TaskEditInp` 均没有 `product_id`,不是单纯前端漏控件,需要先确认任务是否应直接关联商品,再同步迁移、实体、接口和页面。
|
||||
|
||||
### DEF-A-006 会员任务记录缺少任务标题和订单关联
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-SUB-001、UI-A-SUB-004
|
||||
- 实际结果:列表的任务标题为空;详情的任务字段为空;详情未显示订单。
|
||||
- 影响:审核记录无法追溯到具体任务和订单,已结算记录也难以审计。
|
||||
- 数据与代码定位:只读核对最新记录,`task_id` 均为 `0`,但 `trade_order_id` 有值;`task_submission.go` 只按 `s.task_id = t.id` 关联任务,没有通过交易订单/商品兜底,因此标题必然为空。审核逻辑还会再次按 `task_id` 查任务,待审核记录可能直接报“关联任务不存在”。
|
||||
|
||||
### DEF-A-007 充值列表真实姓名缺失
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-RC-001、A-TRADE-012
|
||||
- 实际结果:多条真实充值记录的“真实姓名”列为空,备注中仅显示 `姓名:potter` 等登录名。
|
||||
- 影响:人工审核无法直接核对付款人真实姓名,容易造成误审。
|
||||
|
||||
## 10. 第三轮后台页面执行记录
|
||||
|
||||
### 10.1 主页与门户配置
|
||||
|
||||
| 页面/关联用例 | 状态 | 实际结果 |
|
||||
|---|---|---|
|
||||
| 商城统计首页;UI-A-HOME-001、002 | 失败 | 今日默认区间无汇总;切换近 30 日后能显示指标和微信/支付宝/银行卡渠道数据,但数据截止时间为 `2026-10-11 13:46:46`,页面提示延迟约 107 小时,汇总任务明显停滞 |
|
||||
| 网站设置;UI-A-SITE-001、002 | 部分失败 | 表单字段和保存入口完整,但线上已保存配置中隐私网址为空,大量标记“必填”的交易阈值、次数、佣金和自动派单值均为 0;字段“未订单的支付等待时长”文案不通顺,疑为“未支付订单” |
|
||||
| 文本管理;UI-A-CONTENT-001~004 | 通过 | 初始化数据共 12 条,与原型数量一致;列表已隐藏内容标识;新增、编辑、版本、预览、上下线和删除入口完整;本轮仅打开新增、版本和预览并取消,未写库 |
|
||||
| Banner 设置;UI-A-BANNER-001、002 | 失败 | 6 条记录状态与操作入口可见,但列表图片列全部为空;打开编辑后,必填 Banner 图片也未回显,现有演示记录不满足接口对图片必填的约束 |
|
||||
| 手工商城;UI-A-MANUAL-001~003 | 失败 | 4 条记录可查询、编辑和删除,但图片列全部为空;编辑弹窗的必填展示图片未回显;页面没有发布/下架入口,也不显示发布状态 |
|
||||
|
||||
### 10.2 会员管理补测
|
||||
|
||||
| 页面/关联用例 | 状态 | 实际结果 |
|
||||
|---|---|---|
|
||||
| 会员列表;UI-A-MEMBER-001、002 | 部分失败 | 17 条会员可查询,类别与派单模式显示正确,操作列横向滚动条可用;但页面声明“会员资料默认脱敏展示”,列表和详情仍直接显示完整手机号 |
|
||||
| 会员详情敏感信息;UI-A-MEMBER-003 | 失败 | 详情存在“授权查看明文”按钮,但尚未授权时手机号已经是明文,授权入口没有起到敏感数据隔离作用 |
|
||||
| 会员银行卡信息;UI-A-MEMBER-004、C-FIN-039 | 失败 | 查询 `potter` 后打开银行卡信息,只有银行名 `icbc` 回显;开户人、开户电话、卡号、支付宝/微信收款码全部为空,与提现页已存在的账户及数据库记录不一致 |
|
||||
| 会员等级;UI-A-LEVEL-001~003 | 部分通过 | 7 个初始化等级、价格、赠送金额、接单/提现次数、佣金比例、会员数和编辑/启停/删除入口正常;图标列均为空;删除有二次确认并提示仅无会员且未被任务大厅引用时可删,本轮取消未提交 |
|
||||
| 任务大厅;UI-A-HALL-001~003 | 失败 | 6 条初始化记录可编辑/删除,但图标列为空;后端已有发布状态和状态接口,页面却不显示状态,也没有发布/下架操作 |
|
||||
| 后台用户;UI-A-ADMIN-001、002 | 部分通过 | 菜单真实路径为 `/org/user`,新增表单字段完整;空表单提交能阻止保存并提示填写用户名/密码;当前角色范围列表为 0 条,编辑、状态和删除无法继续验证 |
|
||||
| 管理后台登录日志;UI-A-LOG-001~003 | 通过 | 共 30 条记录,查询、重置、展开、导出、详情、单条/批量删除入口完整;本轮未删除日志 |
|
||||
|
||||
### 10.3 本轮操作安全说明
|
||||
|
||||
- 文本新增、Banner 编辑、手工商城编辑仅打开弹窗查看字段后取消,没有保存。
|
||||
- 首页只切换统计周期,没有触发交易、审核或资金操作。
|
||||
- 角色权限上一轮的勾选联动验证未提交;客服状态验证后已恢复原状态。
|
||||
|
||||
## 11. 第三轮新增缺陷
|
||||
|
||||
### DEF-A-008 首页汇总任务停滞,统计数据延迟 107 小时
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-HOME-001、UI-A-HOME-002
|
||||
- 实际结果:近 30 日指标可以加载,但数据截至 `2026-10-11 13:46:46`,在 `2026-10-16` 验收时页面提示延迟约 107 小时。
|
||||
- 影响:首页经营指标无法反映当前订单、充值、提现和余额情况,不能作为运营决策依据。
|
||||
- 代码定位:仓库只有 `hg_dashboard_daily_stat` 的读取逻辑和演示数据 seed,未检索到生产汇总表的写入/刷新任务;需确认是否由仓库外 ETL 承担,如无则缺少汇总任务实现。
|
||||
|
||||
### DEF-A-009 网站设置存在不完整的线上配置
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-SITE-001、UI-A-SITE-002
|
||||
- 实际结果:隐私网址为空;多个必填业务参数为 0;网站名称仍为“演示站点-3”。
|
||||
- 影响:隐私协议入口、订单等待、交易限制、佣金和自动派单等 C 端行为可能失效或采用异常零值。
|
||||
- 建议:发布前建立配置完整性校验和默认值迁移;加载历史无效配置时给出显著告警,不允许无提示继续运行。
|
||||
|
||||
### DEF-A-010 Banner 与手工商城初始化记录缺少必填图片
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-BANNER-001、UI-A-BANNER-002、UI-A-MANUAL-001、UI-A-MANUAL-002
|
||||
- 实际结果:两个列表的图片列均为空,编辑弹窗也没有回显图片;后端过滤逻辑却要求 Banner `imageUrl`、手工商品 `coverUrl` 必填。
|
||||
- 影响:C 端可能出现空白横幅/商品图;旧记录打开后可能无法在不重新上传图片的情况下保存。
|
||||
- 建议:修正初始化数据并补数据校验脚本;列表为空图时显示明确占位和“图片缺失”告警。
|
||||
|
||||
### DEF-A-011 手工商城发布状态功能未接入页面
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-MANUAL-001、UI-A-MANUAL-003
|
||||
- 实际结果:后端已有 `ManualProductStatus` 接口和发布状态字段,但列表不显示状态、没有发布/下架按钮;编辑接口还会把商品直接改为已发布。
|
||||
- 影响:运营无法在手工商城页面控制上下架,编辑草稿或已下架商品会绕过明确确认直接发布。
|
||||
- 建议:列表增加状态列和发布/下架操作;编辑仅保存内容,不隐式改变发布状态。
|
||||
|
||||
### DEF-A-012 会员银行卡编辑页未回显已有敏感账户资料
|
||||
|
||||
- 严重级别:P0
|
||||
- 关联用例:UI-A-MEMBER-004、UI-C-FORM-003、C-FIN-039、DEF-C-001
|
||||
- 复现步骤:会员列表查询 `potter`,点击“银行卡信息”。
|
||||
- 实际结果:仅开户银行 `icbc` 回显;开户人、开户电话、银行卡号、支付宝与微信收款码均为空。
|
||||
- 预期结果:经过敏感信息授权后完整回显;未授权时至少显示脱敏值和已有附件占位,且禁止空值覆盖密文。
|
||||
- 影响:后台人员无法核对已有账户,直接保存存在把已有敏感字段和附件清空的高风险;同时解释了 C 端绑定信息页为何显示空表单。
|
||||
- 建议:优先检查 finance key 与历史数据加密密钥是否一致、详情接口的解密错误是否被吞掉;编辑接口必须采用“未传不更新”语义,附件 ID 不得因前端未回显而被置空。
|
||||
|
||||
### DEF-A-013 任务大厅发布状态功能未接入页面
|
||||
|
||||
- 严重级别:P1
|
||||
- 关联用例:UI-A-HALL-001、UI-A-HALL-003
|
||||
- 实际结果:列表不显示发布状态,仅有编辑和删除;草稿、待审、已发布、已下线、已归档等状态只能从演示名称猜测。
|
||||
- 代码定位:后端 `task_hall.go` 已实现 `Status`,前端 API 也定义了 `/taskHall/status`,但 `app-member/task-hall/index.vue` 未导入或调用 `Status`,操作列只有编辑/删除;新增大厅还会直接设为已发布。
|
||||
- 影响:运营无法进行清晰的发布/下架流程,新增内容绕过发布确认直接对 C 端生效。
|
||||
- 建议:增加状态列和发布/下架按钮;新增与编辑只保存内容,发布必须走单独状态接口并保留审计记录。
|
||||
|
||||
## 12. 当前阶段结论与修复优先级
|
||||
|
||||
- 当前累计登记 14 项,其中 13 个有效缺陷(P0 1 个、P1 11 个、P2 1 个)和 1 项已接受设计;C 端收款账户回显缺陷与 P0 后台问题属于同一根因链,合并修复与回归。
|
||||
- 第一优先级:DEF-A-012 / DEF-C-001,先恢复历史收款账户的安全解密与回显,并防止空表单覆盖密文和附件。
|
||||
- 第二优先级:DEF-A-006、DEF-A-008,修复任务审核关联和首页汇总任务,否则审核与运营数据均不可信。
|
||||
- 发布前必须处理:网站配置完整性、Banner/商品图片缺失、手工商城与任务大厅发布状态入口。
|
||||
|
||||
## 13. 缺陷修复实施记录
|
||||
|
||||
| 编号 | 处理结果 | 修复内容 |
|
||||
|---|---|---|
|
||||
| DEF-C-001 / DEF-A-012 | 已修复,待发布回归 | C 端详情接口在历史密钥不可用时回退脱敏字段,不再因单条解密失败让整页为空;后台回显姓名、身份证、手机、账号、数字人民币和双收款码脱敏信息;敏感字段和附件采用“未传不更新”语义,并保留授权查看入口 |
|
||||
| DEF-A-001 | 已修复 | URL 地址 Excel 导入弹窗增加底部取消按钮 |
|
||||
| DEF-A-002 | 已修复 | 订单详情增加状态、创建/更新时间、状态时间线、资金流水和审批动作 |
|
||||
| DEF-A-003 | 已接受设计 | 产品方确认允许展示完整手机号,不修改 |
|
||||
| DEF-A-004 | 已修复 | 订单列表新增自动/手动派单模式筛选,后端同步按规则快照查询 |
|
||||
| DEF-A-005 | 已修复 | `hg_task` 增加 `product_id` 迁移并重新生成 DAO/实体;任务新增/编辑增加商品选择、校验和列表回显 |
|
||||
| DEF-A-006 | 已修复 | 审核列表通过交易订单兜底任务标题并显示订单号/商品;`task_id=0` 时按订单归属校验后允许审核,不再误报任务不存在 |
|
||||
| DEF-A-007 | 已修复 | 充值真实姓名优先取实名资料,缺失时回退默认银行卡开户人脱敏值 |
|
||||
| DEF-A-008 | 已修复 | 增加 `dashboard_daily_summary` 汇总任务并注册到 HotGo 定时任务管理;默认每 10 分钟重算今天和昨天,可在后台启停、改表达式和手动执行 |
|
||||
| DEF-A-009 | 已修复 | 后端强制校验隐私地址、交易余额、等待时长、次数、最低充值和自动派单参数;迁移补齐历史零值并修正文案 |
|
||||
| DEF-A-010 | 已修复 | 增加随服务部署的 Banner/商品占位资源,迁移只补齐历史空图片记录 |
|
||||
| DEF-A-011 | 已修复 | 手工商城显示发布状态并增加发布/下架;新增默认草稿,编辑不再隐式发布 |
|
||||
| DEF-A-013 | 已修复 | 任务大厅显示发布状态并增加发布/下线;新增默认草稿 |
|
||||
|
||||
### 13.1 修复后自动化验证
|
||||
|
||||
- 管理后台:`go test ./internal/logic/sys ./internal/crons ./internal/model/input/sysin` 通过。
|
||||
- 管理后台前端:`npm run build` 生产构建通过,6728 个模块完成转换。
|
||||
- C 端:`go test ./internal/finance ./internal/store` 通过;新增“历史密钥不可用时回退脱敏数据”回归用例。
|
||||
- C 端前端:`npm run build` 通过,4186 个模块完成转换。
|
||||
- 新增迁移已在本地 MySQL 8.4 验证;定时任务记录成功初始化,Banner 和商品空图片数量均变为 0。
|
||||
- 页面回归暂未完成:执行机器再次进入锁屏状态,无法继续通过浏览器点击;服务与前端均已在本地启动,解锁后可直接续测。
|
||||
@@ -0,0 +1,34 @@
|
||||
package crons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"hotgo/internal/library/cron"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cron.Register(DashboardDailySummary)
|
||||
}
|
||||
|
||||
var DashboardDailySummary = &cDashboardDailySummary{name: "dashboard_daily_summary"}
|
||||
|
||||
type cDashboardDailySummary struct{ name string }
|
||||
|
||||
func (c *cDashboardDailySummary) GetName() string { return c.name }
|
||||
|
||||
func (c *cDashboardDailySummary) Execute(ctx context.Context, parser *cron.Parser) error {
|
||||
days := 2
|
||||
if len(parser.Args) > 0 {
|
||||
if value, err := strconv.Atoi(parser.Args[0]); err == nil {
|
||||
days = value
|
||||
}
|
||||
}
|
||||
if err := service.SysDashboard().RefreshDaily(ctx, days); err != nil {
|
||||
parser.Logger.Warningf(ctx, "dashboard daily summary failed days:%d err:%+v", days, err)
|
||||
return err
|
||||
}
|
||||
parser.Logger.Infof(ctx, "dashboard daily summary completed days:%d", days)
|
||||
return nil
|
||||
}
|
||||
@@ -21,54 +21,70 @@ type AppMemberPaymentAccountDao struct {
|
||||
|
||||
// AppMemberPaymentAccountColumns defines and stores column names for the table hg_app_member_payment_account.
|
||||
type AppMemberPaymentAccountColumns struct {
|
||||
Id string // 主键
|
||||
TenantId string // 租户ID
|
||||
MemberId string // 会员ID
|
||||
AccountType string // 账户类型
|
||||
BankCode string // 银行编码
|
||||
BankName string // 银行名称
|
||||
HolderCipher string // 持有人密文
|
||||
HolderMask string // 持有人脱敏值
|
||||
MobileCipher string // 预留手机号密文
|
||||
MobileMask string // 预留手机号脱敏值
|
||||
AccountNoCipher string // 账号密文
|
||||
AccountNoHash string // 账号检索哈希
|
||||
AccountNoMask string // 账号脱敏值
|
||||
QrAttachmentId string // 收款码附件ID
|
||||
VerifyStatus string // 核验状态
|
||||
IsDefault string // 默认账户
|
||||
CreatedBy string // 创建者
|
||||
UpdatedBy string // 更新者
|
||||
DeletedBy string // 删除者
|
||||
CreatedAt string // 创建时间
|
||||
UpdatedAt string // 更新时间
|
||||
DeletedAt string // 删除时间
|
||||
Id string // 主键
|
||||
TenantId string // 租户ID
|
||||
MemberId string // 会员ID
|
||||
AccountType string // 账户类型
|
||||
BankCode string // 银行编码
|
||||
BankName string // 银行名称
|
||||
HolderCipher string // 持有人密文
|
||||
HolderMask string // 持有人脱敏值
|
||||
IdentityNoCipher string // 身份证号密文
|
||||
IdentityNoHash string // 身份证号检索哈希
|
||||
IdentityNoMask string // 身份证号脱敏值
|
||||
MobileCipher string // 预留手机号密文
|
||||
MobileMask string // 预留手机号脱敏值
|
||||
AccountNoCipher string // 账号密文
|
||||
AccountNoHash string // 账号检索哈希
|
||||
AccountNoMask string // 账号脱敏值
|
||||
DigitalRmbCodeCipher string // 数字人民币编码密文
|
||||
DigitalRmbCodeHash string // 数字人民币编码检索哈希
|
||||
DigitalRmbCodeMask string // 数字人民币编码脱敏值
|
||||
QrAttachmentId string // 收款码附件ID
|
||||
WechatQrAttachmentId string // 微信二维码附件ID
|
||||
AlipayQrAttachmentId string // 支付宝二维码附件ID
|
||||
VerifyStatus string // 核验状态
|
||||
IsDefault string // 默认账户
|
||||
CreatedBy string // 创建者
|
||||
UpdatedBy string // 更新者
|
||||
DeletedBy string // 删除者
|
||||
CreatedAt string // 创建时间
|
||||
UpdatedAt string // 更新时间
|
||||
DeletedAt string // 删除时间
|
||||
}
|
||||
|
||||
// appMemberPaymentAccountColumns holds the columns for the table hg_app_member_payment_account.
|
||||
var appMemberPaymentAccountColumns = AppMemberPaymentAccountColumns{
|
||||
Id: "id",
|
||||
TenantId: "tenant_id",
|
||||
MemberId: "member_id",
|
||||
AccountType: "account_type",
|
||||
BankCode: "bank_code",
|
||||
BankName: "bank_name",
|
||||
HolderCipher: "holder_cipher",
|
||||
HolderMask: "holder_mask",
|
||||
MobileCipher: "mobile_cipher",
|
||||
MobileMask: "mobile_mask",
|
||||
AccountNoCipher: "account_no_cipher",
|
||||
AccountNoHash: "account_no_hash",
|
||||
AccountNoMask: "account_no_mask",
|
||||
QrAttachmentId: "qr_attachment_id",
|
||||
VerifyStatus: "verify_status",
|
||||
IsDefault: "is_default",
|
||||
CreatedBy: "created_by",
|
||||
UpdatedBy: "updated_by",
|
||||
DeletedBy: "deleted_by",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
DeletedAt: "deleted_at",
|
||||
Id: "id",
|
||||
TenantId: "tenant_id",
|
||||
MemberId: "member_id",
|
||||
AccountType: "account_type",
|
||||
BankCode: "bank_code",
|
||||
BankName: "bank_name",
|
||||
HolderCipher: "holder_cipher",
|
||||
HolderMask: "holder_mask",
|
||||
IdentityNoCipher: "identity_no_cipher",
|
||||
IdentityNoHash: "identity_no_hash",
|
||||
IdentityNoMask: "identity_no_mask",
|
||||
MobileCipher: "mobile_cipher",
|
||||
MobileMask: "mobile_mask",
|
||||
AccountNoCipher: "account_no_cipher",
|
||||
AccountNoHash: "account_no_hash",
|
||||
AccountNoMask: "account_no_mask",
|
||||
DigitalRmbCodeCipher: "digital_rmb_code_cipher",
|
||||
DigitalRmbCodeHash: "digital_rmb_code_hash",
|
||||
DigitalRmbCodeMask: "digital_rmb_code_mask",
|
||||
QrAttachmentId: "qr_attachment_id",
|
||||
WechatQrAttachmentId: "wechat_qr_attachment_id",
|
||||
AlipayQrAttachmentId: "alipay_qr_attachment_id",
|
||||
VerifyStatus: "verify_status",
|
||||
IsDefault: "is_default",
|
||||
CreatedBy: "created_by",
|
||||
UpdatedBy: "updated_by",
|
||||
DeletedBy: "deleted_by",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
DeletedAt: "deleted_at",
|
||||
}
|
||||
|
||||
// NewAppMemberPaymentAccountDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -25,6 +25,7 @@ type TaskColumns struct {
|
||||
TenantId string // 租户ID
|
||||
TaskNo string // 任务编号
|
||||
HallId string // 任务大厅ID
|
||||
ProductId string // 关联商品ID
|
||||
Title string // 标题
|
||||
Description string // 描述
|
||||
DetailHtml string // 清洗后的任务详情
|
||||
@@ -54,6 +55,7 @@ var taskColumns = TaskColumns{
|
||||
TenantId: "tenant_id",
|
||||
TaskNo: "task_no",
|
||||
HallId: "hall_id",
|
||||
ProductId: "product_id",
|
||||
Title: "title",
|
||||
Description: "description",
|
||||
DetailHtml: "detail_html",
|
||||
|
||||
@@ -321,7 +321,12 @@ func (s *sSysAppMember) Category(ctx context.Context, in *sysin.AppMemberCategor
|
||||
|
||||
func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaymentListInp) (list []*sysin.AppMemberPaymentModel, err error) {
|
||||
c := dao.AppMemberPaymentAccount.Columns()
|
||||
err = s.paymentModel(ctx).Fields(c.Id, c.MemberId, c.AccountType, c.BankCode, c.BankName, c.HolderMask, c.MobileMask, c.AccountNoMask, c.QrAttachmentId, c.VerifyStatus, c.IsDefault, c.CreatedAt, c.UpdatedAt).Where(c.MemberId, in.MemberId).OrderDesc(c.IsDefault).OrderDesc(c.Id).Scan(&list)
|
||||
ac := dao.SysAttachment.Columns()
|
||||
err = s.paymentModel(ctx).As("p").
|
||||
LeftJoin(dao.SysAttachment.Table()+" wq", col("wq", ac.Id)+"="+col("p", c.WechatQrAttachmentId)).
|
||||
LeftJoin(dao.SysAttachment.Table()+" aq", col("aq", ac.Id)+"="+col("p", c.AlipayQrAttachmentId)).
|
||||
Fields(col("p", c.Id), col("p", c.MemberId), col("p", c.AccountType), col("p", c.BankCode), col("p", c.BankName), col("p", c.HolderMask), col("p", c.IdentityNoMask), col("p", c.MobileMask), col("p", c.AccountNoMask), col("p", c.DigitalRmbCodeMask), col("p", c.QrAttachmentId), col("p", c.WechatQrAttachmentId), col("p", c.AlipayQrAttachmentId), fmt.Sprintf("COALESCE(%s,'') wechat_qr_url", col("wq", ac.FileUrl)), fmt.Sprintf("COALESCE(%s,'') alipay_qr_url", col("aq", ac.FileUrl)), col("p", c.VerifyStatus), col("p", c.IsDefault), col("p", c.CreatedAt), col("p", c.UpdatedAt)).
|
||||
Where(col("p", c.MemberId), in.MemberId).OrderDesc(col("p", c.IsDefault)).OrderDesc(col("p", c.Id)).Scan(&list)
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, "获取收款账户失败")
|
||||
}
|
||||
@@ -330,7 +335,23 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
|
||||
|
||||
func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaymentEditInp) error {
|
||||
c := dao.AppMemberPaymentAccount.Columns()
|
||||
data := g.Map{c.MemberId: in.MemberId, c.AccountType: in.AccountType, c.BankCode: in.BankCode, c.BankName: in.BankName, c.QrAttachmentId: in.QrAttachmentId, c.VerifyStatus: in.VerifyStatus, c.IsDefault: in.IsDefault, c.UpdatedBy: contexts.GetUserId(ctx)}
|
||||
attachmentColumns := dao.SysAttachment.Columns()
|
||||
if in.WechatQrAttachmentId == 0 && in.WechatQrUrl != "" {
|
||||
_ = dao.SysAttachment.Ctx(ctx).Fields(attachmentColumns.Id).Where(attachmentColumns.FileUrl, in.WechatQrUrl).OrderDesc(attachmentColumns.Id).Limit(1).Scan(&in.WechatQrAttachmentId)
|
||||
}
|
||||
if in.AlipayQrAttachmentId == 0 && in.AlipayQrUrl != "" {
|
||||
_ = dao.SysAttachment.Ctx(ctx).Fields(attachmentColumns.Id).Where(attachmentColumns.FileUrl, in.AlipayQrUrl).OrderDesc(attachmentColumns.Id).Limit(1).Scan(&in.AlipayQrAttachmentId)
|
||||
}
|
||||
data := g.Map{c.MemberId: in.MemberId, c.AccountType: in.AccountType, c.BankCode: in.BankCode, c.BankName: in.BankName, c.VerifyStatus: in.VerifyStatus, c.IsDefault: in.IsDefault, c.UpdatedBy: contexts.GetUserId(ctx)}
|
||||
if in.QrAttachmentId > 0 {
|
||||
data[c.QrAttachmentId] = in.QrAttachmentId
|
||||
}
|
||||
if in.WechatQrAttachmentId > 0 {
|
||||
data[c.WechatQrAttachmentId] = in.WechatQrAttachmentId
|
||||
}
|
||||
if in.AlipayQrAttachmentId > 0 {
|
||||
data[c.AlipayQrAttachmentId] = in.AlipayQrAttachmentId
|
||||
}
|
||||
if in.Holder != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.Holder)
|
||||
if e != nil {
|
||||
@@ -347,6 +368,19 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
|
||||
data[c.MobileCipher] = v
|
||||
data[c.MobileMask] = encrypt.MaskMobile(in.Mobile)
|
||||
}
|
||||
if in.IdentityNo != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.IdentityNo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
h, e := encrypt.HashField(ctx, in.IdentityNo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
data[c.IdentityNoCipher] = v
|
||||
data[c.IdentityNoHash] = h
|
||||
data[c.IdentityNoMask] = encrypt.MaskAccount(in.IdentityNo)
|
||||
}
|
||||
if in.AccountNo != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.AccountNo)
|
||||
if e != nil {
|
||||
@@ -360,6 +394,19 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
|
||||
data[c.AccountNoHash] = h
|
||||
data[c.AccountNoMask] = encrypt.MaskAccount(in.AccountNo)
|
||||
}
|
||||
if in.DigitalRMBCode != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.DigitalRMBCode)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
h, e := encrypt.HashField(ctx, in.DigitalRMBCode)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
data[c.DigitalRmbCodeCipher] = v
|
||||
data[c.DigitalRmbCodeHash] = h
|
||||
data[c.DigitalRmbCodeMask] = encrypt.MaskAccount(in.DigitalRMBCode)
|
||||
}
|
||||
if in.IsDefault == 1 {
|
||||
_, _ = s.paymentModel(ctx).Where(c.MemberId, in.MemberId).Data(g.Map{c.IsDefault: 2}).Update()
|
||||
}
|
||||
@@ -370,7 +417,7 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
|
||||
}
|
||||
} else {
|
||||
data[c.CreatedBy] = contexts.GetUserId(ctx)
|
||||
_, err := s.paymentModel(ctx).Fields(c.MemberId, c.AccountType, c.BankCode, c.BankName, c.HolderCipher, c.HolderMask, c.MobileCipher, c.MobileMask, c.AccountNoCipher, c.AccountNoHash, c.AccountNoMask, c.QrAttachmentId, c.VerifyStatus, c.IsDefault, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
|
||||
_, err := s.paymentModel(ctx).Fields(c.MemberId, c.AccountType, c.BankCode, c.BankName, c.HolderCipher, c.HolderMask, c.IdentityNoCipher, c.IdentityNoHash, c.IdentityNoMask, c.MobileCipher, c.MobileMask, c.AccountNoCipher, c.AccountNoHash, c.AccountNoMask, c.DigitalRmbCodeCipher, c.DigitalRmbCodeHash, c.DigitalRmbCodeMask, c.QrAttachmentId, c.WechatQrAttachmentId, c.AlipayQrAttachmentId, c.VerifyStatus, c.IsDefault, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "创建收款账户失败")
|
||||
}
|
||||
@@ -465,8 +512,25 @@ func (s *sSysAppMember) Sensitive(ctx context.Context, in *sysin.AppMemberSensit
|
||||
var row *entity.AppMemberPaymentAccount
|
||||
if err = s.paymentModel(ctx).WherePri(in.TargetId).Scan(&row); err == nil && row != nil {
|
||||
res.Fields["holder"], _ = encrypt.DecryptField(ctx, row.HolderCipher)
|
||||
res.Fields["identityNo"], _ = encrypt.DecryptField(ctx, row.IdentityNoCipher)
|
||||
res.Fields["mobile"], _ = encrypt.DecryptField(ctx, row.MobileCipher)
|
||||
res.Fields["accountNo"], err = encrypt.DecryptField(ctx, row.AccountNoCipher)
|
||||
res.Fields["accountNo"], _ = encrypt.DecryptField(ctx, row.AccountNoCipher)
|
||||
res.Fields["digitalRmbCode"], _ = encrypt.DecryptField(ctx, row.DigitalRmbCodeCipher)
|
||||
if res.Fields["holder"] == "" {
|
||||
res.Fields["holder"] = row.HolderMask
|
||||
}
|
||||
if res.Fields["identityNo"] == "" {
|
||||
res.Fields["identityNo"] = row.IdentityNoMask
|
||||
}
|
||||
if res.Fields["mobile"] == "" {
|
||||
res.Fields["mobile"] = row.MobileMask
|
||||
}
|
||||
if res.Fields["accountNo"] == "" {
|
||||
res.Fields["accountNo"] = row.AccountNoMask
|
||||
}
|
||||
if res.Fields["digitalRmbCode"] == "" {
|
||||
res.Fields["digitalRmbCode"] = row.DigitalRmbCodeMask
|
||||
}
|
||||
}
|
||||
case "address":
|
||||
var row *entity.AppMemberAddress
|
||||
|
||||
@@ -2,6 +2,8 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
"hotgo/internal/model/entity"
|
||||
@@ -11,10 +13,20 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type dashboardAggregateSpec struct {
|
||||
DimensionType string
|
||||
DimensionValue string
|
||||
MetricCode string
|
||||
SQL string
|
||||
Args []interface{}
|
||||
}
|
||||
|
||||
const (
|
||||
dashboardDimensionGlobal = "global"
|
||||
dashboardDimensionChannel = "channel"
|
||||
@@ -54,6 +66,97 @@ func NewSysDashboard() *sSysDashboard { return &sSysDashboard{} }
|
||||
|
||||
func init() { service.RegisterSysDashboard(NewSysDashboard()) }
|
||||
|
||||
// RefreshDaily rebuilds recent dashboard snapshots from business source tables.
|
||||
func (s *sSysDashboard) RefreshDaily(ctx context.Context, days int) error {
|
||||
if days < 1 {
|
||||
days = 1
|
||||
}
|
||||
if days > 90 {
|
||||
days = 90
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for offset := days - 1; offset >= 0; offset-- {
|
||||
day := time.Now().AddDate(0, 0, -offset)
|
||||
start := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, day.Location())
|
||||
end := start.AddDate(0, 0, 1)
|
||||
if err := s.refreshDashboardDay(ctx, start, end); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysDashboard) refreshDashboardDay(ctx context.Context, start, end time.Time) error {
|
||||
dc := dao.DashboardDailyStat.Columns()
|
||||
if _, err := dao.DashboardDailyStat.Ctx(ctx).Where(dc.StatDate, start.Format("2006-01-02")).Delete(); err != nil {
|
||||
return gerror.Wrap(err, "清理看板日汇总失败")
|
||||
}
|
||||
mc, pc, oc := dao.AppMember.Columns(), dao.Product.Columns(), dao.TradeOrder.Columns()
|
||||
rc, wc, lc := dao.RechargeOrder.Columns(), dao.WithdrawalOrder.Columns(), dao.WalletLedger.Columns()
|
||||
wac, cc := dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
|
||||
specs := []dashboardAggregateSpec{
|
||||
{dashboardDimensionGlobal, "all", "member_total", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,0 metric_amount FROM %s WHERE %s IS NULL AND %s<? GROUP BY %s", mc.TenantId, dao.AppMember.Table(), mc.DeletedAt, mc.CreatedAt, mc.TenantId), []interface{}{end}},
|
||||
{dashboardDimensionGlobal, "all", "member_new", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,0 metric_amount FROM %s WHERE %s IS NULL AND %s>=? AND %s<? GROUP BY %s", mc.TenantId, dao.AppMember.Table(), mc.DeletedAt, mc.CreatedAt, mc.CreatedAt, mc.TenantId), []interface{}{start, end}},
|
||||
{dashboardDimensionGlobal, "all", "product_total", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,0 metric_amount FROM %s WHERE %s IS NULL AND %s<? GROUP BY %s", pc.TenantId, dao.Product.Table(), pc.DeletedAt, pc.CreatedAt, pc.TenantId), []interface{}{end}},
|
||||
{dashboardDimensionGlobal, "all", "trade_order", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s>=? AND %s<? GROUP BY %s", oc.TenantId, oc.TotalAmount, dao.TradeOrder.Table(), oc.CreatedAt, oc.CreatedAt, oc.TenantId), []interface{}{start, end}},
|
||||
{dashboardDimensionGlobal, "all", "recharge_member", fmt.Sprintf("SELECT %s tenant_id,COUNT(DISTINCT %s) metric_count,0 metric_amount FROM %s WHERE %s=? AND %s>=? AND %s<? GROUP BY %s", rc.TenantId, rc.MemberId, dao.RechargeOrder.Table(), rc.RechargeStatus, rc.CreditedAt, rc.CreditedAt, rc.TenantId), []interface{}{consts.RechargeSuccess, start, end}},
|
||||
{dashboardDimensionGlobal, "all", "recharge_success", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s=? AND %s>=? AND %s<? GROUP BY %s", rc.TenantId, rc.PaidAmount, dao.RechargeOrder.Table(), rc.RechargeStatus, rc.CreditedAt, rc.CreditedAt, rc.TenantId), []interface{}{consts.RechargeSuccess, start, end}},
|
||||
{dashboardDimensionGlobal, "all", "recharge_pending", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s IN (?,?) AND %s<? GROUP BY %s", rc.TenantId, rc.RequestAmount, dao.RechargeOrder.Table(), rc.RechargeStatus, rc.CreatedAt, rc.TenantId), []interface{}{consts.RechargeUnpaid, consts.RechargeReview, end}},
|
||||
{dashboardDimensionGlobal, "all", "withdrawal_success", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s=? AND %s>=? AND %s<? GROUP BY %s", wc.TenantId, wc.ActualAmount, dao.WithdrawalOrder.Table(), wc.WithdrawalStatus, wc.CompletedAt, wc.CompletedAt, wc.TenantId), []interface{}{consts.WithdrawalSuccess, start, end}},
|
||||
{dashboardDimensionGlobal, "all", "withdrawal_pending", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s IN (?,?,?) AND %s<? GROUP BY %s", wc.TenantId, wc.ActualAmount, dao.WithdrawalOrder.Table(), wc.WithdrawalStatus, wc.CreatedAt, wc.TenantId), []interface{}{consts.WithdrawalPending, consts.WithdrawalReviewing, consts.WithdrawalProcessing, end}},
|
||||
{dashboardDimensionGlobal, "all", "commission", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s='commission' AND %s>=? AND %s<? GROUP BY %s", lc.TenantId, lc.Amount, dao.WalletLedger.Table(), lc.BizType, lc.OccurredAt, lc.OccurredAt, lc.TenantId), []interface{}{start, end}},
|
||||
{dashboardDimensionGlobal, "all", "referral_commission", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s='referral_commission' AND %s>=? AND %s<? GROUP BY %s", lc.TenantId, lc.Amount, dao.WalletLedger.Table(), lc.BizType, lc.OccurredAt, lc.OccurredAt, lc.TenantId), []interface{}{start, end}},
|
||||
{dashboardDimensionGlobal, "all", "interest_transfer", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s='interest_transfer' AND %s>=? AND %s<? GROUP BY %s", lc.TenantId, lc.Amount, dao.WalletLedger.Table(), lc.BizType, lc.OccurredAt, lc.OccurredAt, lc.TenantId), []interface{}{start, end}},
|
||||
{dashboardDimensionGlobal, "all", "wallet_available", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s='CNY' GROUP BY %s", wac.TenantId, wac.AvailableAmount, dao.WalletAccount.Table(), wac.Currency, wac.TenantId), nil},
|
||||
{dashboardDimensionGlobal, "all", "wallet_frozen", fmt.Sprintf("SELECT %s tenant_id,COUNT(*) metric_count,COALESCE(SUM(%s),0) metric_amount FROM %s WHERE %s='CNY' GROUP BY %s", wac.TenantId, wac.FrozenAmount, dao.WalletAccount.Table(), wac.Currency, wac.TenantId), nil},
|
||||
}
|
||||
for _, spec := range specs {
|
||||
if err := s.insertDashboardAggregate(ctx, start, spec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
channelSpecs := []dashboardAggregateSpec{
|
||||
{dashboardDimensionChannel, "", "recharge_success", fmt.Sprintf("SELECT r.%s tenant_id,COUNT(*) metric_count,COALESCE(SUM(r.%s),0) metric_amount,c.%s dimension_value FROM %s r LEFT JOIN %s c ON c.%s=r.%s WHERE r.%s=? AND r.%s>=? AND r.%s<? GROUP BY r.%s,c.%s", rc.TenantId, rc.PaidAmount, cc.ChannelCode, dao.RechargeOrder.Table(), dao.PaymentChannel.Table(), cc.Id, rc.ChannelId, rc.RechargeStatus, rc.CreditedAt, rc.CreditedAt, rc.TenantId, cc.ChannelCode), []interface{}{consts.RechargeSuccess, start, end}},
|
||||
{dashboardDimensionChannel, "", "withdrawal_success", fmt.Sprintf("SELECT w.%s tenant_id,COUNT(*) metric_count,COALESCE(SUM(w.%s),0) metric_amount,c.%s dimension_value FROM %s w LEFT JOIN %s c ON c.%s=w.%s WHERE w.%s=? AND w.%s>=? AND w.%s<? GROUP BY w.%s,c.%s", wc.TenantId, wc.ActualAmount, cc.ChannelCode, dao.WithdrawalOrder.Table(), dao.PaymentChannel.Table(), cc.Id, wc.ChannelId, wc.WithdrawalStatus, wc.CompletedAt, wc.CompletedAt, wc.TenantId, cc.ChannelCode), []interface{}{consts.WithdrawalSuccess, start, end}},
|
||||
}
|
||||
for _, spec := range channelSpecs {
|
||||
if err := s.insertDashboardAggregate(ctx, start, spec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysDashboard) insertDashboardAggregate(ctx context.Context, day time.Time, spec dashboardAggregateSpec) error {
|
||||
type row struct {
|
||||
TenantId uint64 `orm:"tenant_id"`
|
||||
MetricCount int64 `orm:"metric_count"`
|
||||
MetricAmount int64 `orm:"metric_amount"`
|
||||
DimensionValue string `orm:"dimension_value"`
|
||||
}
|
||||
var rows []*row
|
||||
if err := g.DB().Ctx(ctx).Raw(spec.SQL, spec.Args...).Scan(&rows); err != nil {
|
||||
return gerror.Wrapf(err, "汇总看板指标失败 metric:%s", spec.MetricCode)
|
||||
}
|
||||
c := dao.DashboardDailyStat.Columns()
|
||||
now := gtime.Now()
|
||||
for _, item := range rows {
|
||||
dimensionValue := spec.DimensionValue
|
||||
if item.DimensionValue != "" {
|
||||
dimensionValue = item.DimensionValue
|
||||
}
|
||||
if dimensionValue == "" {
|
||||
dimensionValue = "unknown"
|
||||
}
|
||||
_, err := dao.DashboardDailyStat.Ctx(ctx).Fields(c.TenantId, c.StatDate, c.DimensionType, c.DimensionValue, c.MetricCode, c.MetricCount, c.MetricAmount, c.CalculatedAt, c.CreatedAt, c.UpdatedAt).Data(g.Map{c.TenantId: item.TenantId, c.StatDate: day.Format("2006-01-02"), c.DimensionType: spec.DimensionType, c.DimensionValue: dimensionValue, c.MetricCode: spec.MetricCode, c.MetricCount: item.MetricCount, c.MetricAmount: item.MetricAmount, c.CalculatedAt: now, c.CreatedAt: now, c.UpdatedAt: now}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrapf(err, "写入看板指标失败 metric:%s", spec.MetricCode)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysDashboard) Summary(ctx context.Context, in *sysin.DashboardSummaryInp) (res *sysin.DashboardSummaryModel, err error) {
|
||||
start, end := normalizeDashboardRange(in)
|
||||
days := int(end.Sub(start).Hours()/24) + 1
|
||||
|
||||
@@ -498,7 +498,6 @@ func (s *sSysPortal) ManualProductEdit(ctx context.Context, in *sysin.ManualProd
|
||||
c.PriceAmount: in.PriceAmount,
|
||||
c.CoverAttachmentId: in.CoverAttachmentId,
|
||||
c.CoverUrl: in.CoverUrl,
|
||||
c.PublishStatus: consts.BizPublishPublished,
|
||||
c.UpdatedBy: userId,
|
||||
c.Version: gdb.Raw(c.Version + "+1"),
|
||||
}).Update()
|
||||
@@ -512,7 +511,7 @@ func (s *sSysPortal) ManualProductEdit(ctx context.Context, in *sysin.ManualProd
|
||||
return in.Id, nil
|
||||
}
|
||||
in.ProductNo = "MP" + gtime.Now().Format("YmdHis") + grand.S(5)
|
||||
in.PublishStatus = consts.BizPublishPublished
|
||||
in.PublishStatus = consts.BizPublishDraft
|
||||
in.CreatedBy = userId
|
||||
in.UpdatedBy = userId
|
||||
result, e := s.productModel(ctx).Fields(c.ProductNo, c.CategoryId, c.ProductType, c.PaymentType, c.Name, c.ShopName, c.PriceAmount, c.Currency, c.CoverAttachmentId, c.CoverUrl, c.DetailHtml, c.IsRecommended, c.PublishStatus, c.StartAt, c.EndAt, c.Sort, c.Version, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(in).Insert()
|
||||
|
||||
@@ -40,8 +40,10 @@ func (s *sSysRechargeOrder) baseModel(ctx context.Context) *gdb.Model {
|
||||
|
||||
func (s *sSysRechargeOrder) listModel(ctx context.Context) *gdb.Model {
|
||||
rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
|
||||
ac := dao.AppMemberPaymentAccount.Columns()
|
||||
realName := fmt.Sprintf("COALESCE(NULLIF(%s,''),(SELECT %s FROM %s pa WHERE pa.%s=%s AND pa.%s IS NULL ORDER BY pa.%s ASC,pa.%s DESC LIMIT 1),'') real_name_mask", col("p", pc.RealNameMask), ac.HolderMask, dao.AppMemberPaymentAccount.Table(), ac.MemberId, col("r", rc.MemberId), ac.DeletedAt, ac.IsDefault, ac.Id)
|
||||
return s.baseModel(ctx).
|
||||
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
|
||||
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.Mobile), realName, fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
|
||||
}
|
||||
|
||||
func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysin.RechargeOrderListInp) (*gdb.Model, error) {
|
||||
|
||||
@@ -31,8 +31,11 @@ func (s *sSysTask) relationModel(ctx context.Context) *gdb.Model {
|
||||
return handler.Model(dao.TaskLevel.Ctx(ctx), &handler.Option{FilterTenant: true})
|
||||
}
|
||||
func (s *sSysTask) listModel(ctx context.Context) *gdb.Model {
|
||||
tc, hc := dao.Task.Columns(), dao.TaskHall.Columns()
|
||||
return s.Model(ctx).As("t").LeftJoin(dao.TaskHall.Table()+" h", col("h", hc.Id)+"="+col("t", tc.HallId)).Fields("t.*", fmt.Sprintf("COALESCE(%s,'') hall_name", col("h", hc.Name)))
|
||||
tc, hc, pc := dao.Task.Columns(), dao.TaskHall.Columns(), dao.Product.Columns()
|
||||
return s.Model(ctx).As("t").
|
||||
LeftJoin(dao.TaskHall.Table()+" h", col("h", hc.Id)+"="+col("t", tc.HallId)).
|
||||
LeftJoin(dao.Product.Table()+" p", col("p", pc.Id)+"="+col("t", tc.ProductId)).
|
||||
Fields("t.*", fmt.Sprintf("COALESCE(%s,'') hall_name", col("h", hc.Name)), fmt.Sprintf("COALESCE(%s,'') product_name", col("p", pc.Name)))
|
||||
}
|
||||
func (s *sSysTask) List(ctx context.Context, in *sysin.TaskListInp) (list []*sysin.TaskListModel, total int, err error) {
|
||||
tc := dao.Task.Columns()
|
||||
@@ -106,6 +109,14 @@ func (s *sSysTask) Edit(ctx context.Context, in *sysin.TaskEditInp) (id uint64,
|
||||
if hallCount == 0 {
|
||||
return 0, gerror.New("任务大厅不存在")
|
||||
}
|
||||
pc := dao.Product.Columns()
|
||||
productCount, err := handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(in.ProductId).WhereIn(pc.PublishStatus, []int{consts.BizPublishPublished, consts.BizPublishOffline, consts.BizPublishDraft}).Count()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if productCount == 0 {
|
||||
return 0, gerror.New("关联商品不存在")
|
||||
}
|
||||
lc := dao.AppMemberLevel.Columns()
|
||||
levelCount, err := handler.Model(dao.AppMemberLevel.Ctx(ctx), &handler.Option{FilterTenant: true}).WhereIn(lc.Id, in.LevelIds).Where(lc.Status, consts.StatusEnabled).Count()
|
||||
if err != nil {
|
||||
@@ -125,7 +136,7 @@ func (s *sSysTask) Edit(ctx context.Context, in *sysin.TaskEditInp) (id uint64,
|
||||
if before == nil {
|
||||
return gerror.New("任务不存在")
|
||||
}
|
||||
result, e := s.Model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Fields(sysin.TaskUpdateFields{}).Data(g.Map{c.HallId: in.HallId, c.Title: in.Title, c.Description: in.Description, c.DetailHtml: in.DetailHtml, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.CommissionAmount: in.CommissionAmount, c.CommissionRate: in.CommissionRate, c.DailyLimit: in.DailyLimit, c.PerMemberLimit: in.PerMemberLimit, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.Sort: in.Sort, c.Remark: in.Remark, c.UpdatedBy: uid, c.Version: gdb.Raw(c.Version + "+1")}).Update()
|
||||
result, e := s.Model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Fields(sysin.TaskUpdateFields{}).Data(g.Map{c.HallId: in.HallId, c.ProductId: in.ProductId, c.Title: in.Title, c.Description: in.Description, c.DetailHtml: in.DetailHtml, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.CommissionAmount: in.CommissionAmount, c.CommissionRate: in.CommissionRate, c.DailyLimit: in.DailyLimit, c.PerMemberLimit: in.PerMemberLimit, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.Sort: in.Sort, c.Remark: in.Remark, c.UpdatedBy: uid, c.Version: gdb.Raw(c.Version + "+1")}).Update()
|
||||
if e != nil {
|
||||
return gerror.Wrap(e, "更新任务失败")
|
||||
}
|
||||
@@ -136,7 +147,7 @@ func (s *sSysTask) Edit(ctx context.Context, in *sysin.TaskEditInp) (id uint64,
|
||||
id = in.Id
|
||||
} else {
|
||||
taskNo := "TK" + gtime.Now().Format("YmdHis") + grand.S(5)
|
||||
result, e := s.Model(ctx).Fields(c.TaskNo, c.HallId, c.Title, c.Description, c.DetailHtml, c.CoverAttachmentId, c.CoverUrl, c.CommissionAmount, c.CommissionRate, c.DailyLimit, c.PerMemberLimit, c.StartAt, c.EndAt, c.PublishStatus, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.TaskNo: taskNo, c.HallId: in.HallId, c.Title: in.Title, c.Description: in.Description, c.DetailHtml: in.DetailHtml, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.CommissionAmount: in.CommissionAmount, c.CommissionRate: in.CommissionRate, c.DailyLimit: in.DailyLimit, c.PerMemberLimit: in.PerMemberLimit, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.PublishStatus: consts.BizPublishDraft, c.Sort: in.Sort, c.Remark: in.Remark, c.CreatedBy: uid, c.UpdatedBy: uid}).Insert()
|
||||
result, e := s.Model(ctx).Fields(c.TaskNo, c.HallId, c.ProductId, c.Title, c.Description, c.DetailHtml, c.CoverAttachmentId, c.CoverUrl, c.CommissionAmount, c.CommissionRate, c.DailyLimit, c.PerMemberLimit, c.StartAt, c.EndAt, c.PublishStatus, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.TaskNo: taskNo, c.HallId: in.HallId, c.ProductId: in.ProductId, c.Title: in.Title, c.Description: in.Description, c.DetailHtml: in.DetailHtml, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.CommissionAmount: in.CommissionAmount, c.CommissionRate: in.CommissionRate, c.DailyLimit: in.DailyLimit, c.PerMemberLimit: in.PerMemberLimit, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.PublishStatus: consts.BizPublishDraft, c.Sort: in.Sort, c.Remark: in.Remark, c.CreatedBy: uid, c.UpdatedBy: uid}).Insert()
|
||||
if e != nil {
|
||||
return gerror.Wrap(e, "创建任务失败")
|
||||
}
|
||||
@@ -152,7 +163,7 @@ func (s *sSysTask) Edit(ctx context.Context, in *sysin.TaskEditInp) (id uint64,
|
||||
return gerror.Wrap(e, "保存任务等级失败")
|
||||
}
|
||||
}
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task", Action: "task.edit", TargetType: "task", TargetId: id, Before: before, After: g.Map{"title": in.Title, "commissionAmount": in.CommissionAmount, "levelIds": in.LevelIds}})
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task", Action: "task.edit", TargetType: "task", TargetId: id, Before: before, After: g.Map{"title": in.Title, "productId": in.ProductId, "commissionAmount": in.CommissionAmount, "levelIds": in.LevelIds}})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func (s *sSysTaskHall) Edit(ctx context.Context, in *sysin.TaskHallEditInp) erro
|
||||
return gerror.New("大厅数据已更新,请刷新后重试")
|
||||
}
|
||||
} else {
|
||||
data[c.PublishStatus] = consts.BizPublishPublished
|
||||
data[c.PublishStatus] = consts.BizPublishDraft
|
||||
data[c.CreatedBy] = userId
|
||||
result, err := s.model(ctx).Fields(c.HallCode, c.Name, c.IconAttachmentId, c.IconUrl, c.Summary, c.CommissionRate, c.RateUnit, c.ClientRoute, c.PublishStatus, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
|
||||
if err != nil {
|
||||
|
||||
@@ -28,8 +28,13 @@ func (s *sSysTaskSubmission) Model(ctx context.Context) *gdb.Model {
|
||||
return handler.Model(dao.TaskSubmission.Ctx(ctx), &handler.Option{FilterTenant: true})
|
||||
}
|
||||
func (s *sSysTaskSubmission) listModel(ctx context.Context) *gdb.Model {
|
||||
sc, tc, mc, lc := dao.TaskSubmission.Columns(), dao.Task.Columns(), dao.AppMember.Columns(), dao.AppMemberLevel.Columns()
|
||||
return s.Model(ctx).As("s").LeftJoin(dao.Task.Table()+" t", col("t", tc.Id)+"="+col("s", sc.TaskId)).LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("s", sc.MemberId)).LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("s", sc.MemberLevelId)).Fields(col("s", sc.Id), col("s", sc.SubmissionNo), col("s", sc.TradeOrderId), col("s", sc.TaskId), fmt.Sprintf("COALESCE(%s,'') task_title", col("t", tc.Title)), col("s", sc.MemberId), col("m", mc.Username), col("m", mc.Mobile), fmt.Sprintf("COALESCE(%s,'') level_name", col("l", lc.Name)), col("s", sc.EvidenceText), col("s", sc.EvidenceAttachmentIds), col("s", sc.SubmissionStatus), col("s", sc.CommissionAmount), col("s", sc.SuccessRate), col("s", sc.ReviewReasonCode), col("s", sc.ReviewReason), col("s", sc.AllowResubmit), col("s", sc.LedgerId), col("s", sc.ReviewedAt), col("s", sc.SettledAt), col("s", sc.CreatedAt))
|
||||
sc, tc, oc, mc, lc := dao.TaskSubmission.Columns(), dao.Task.Columns(), dao.TradeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberLevel.Columns()
|
||||
return s.Model(ctx).As("s").
|
||||
LeftJoin(dao.Task.Table()+" t", col("t", tc.Id)+"="+col("s", sc.TaskId)).
|
||||
LeftJoin(dao.TradeOrder.Table()+" o", col("o", oc.Id)+"="+col("s", sc.TradeOrderId)).
|
||||
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("s", sc.MemberId)).
|
||||
LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("s", sc.MemberLevelId)).
|
||||
Fields(col("s", sc.Id), col("s", sc.SubmissionNo), col("s", sc.TradeOrderId), fmt.Sprintf("COALESCE(%s,'') trade_order_no", col("o", oc.OrderNo)), fmt.Sprintf("COALESCE(%s,'') product_name", col("o", oc.ProductName)), col("s", sc.TaskId), fmt.Sprintf("COALESCE(NULLIF(%s,''),NULLIF(%s,''),'') task_title", col("t", tc.Title), col("o", oc.ProductName)), col("s", sc.MemberId), col("m", mc.Username), col("m", mc.Mobile), fmt.Sprintf("COALESCE(%s,'') level_name", col("l", lc.Name)), col("s", sc.EvidenceText), col("s", sc.EvidenceAttachmentIds), col("s", sc.SubmissionStatus), col("s", sc.CommissionAmount), col("s", sc.SuccessRate), col("s", sc.ReviewReasonCode), col("s", sc.ReviewReason), col("s", sc.AllowResubmit), col("s", sc.LedgerId), col("s", sc.ReviewedAt), col("s", sc.SettledAt), col("s", sc.CreatedAt))
|
||||
}
|
||||
func (s *sSysTaskSubmission) filter(ctx context.Context, mod *gdb.Model, in *sysin.TaskSubmissionListInp) (*gdb.Model, error) {
|
||||
sc, mc := dao.TaskSubmission.Columns(), dao.AppMember.Columns()
|
||||
@@ -82,12 +87,27 @@ func (s *sSysTaskSubmission) Review(ctx context.Context, in *sysin.TaskSubmissio
|
||||
if submission.SubmissionStatus != consts.TaskSubmissionPending {
|
||||
return gerror.New("该记录已审核,请勿重复操作")
|
||||
}
|
||||
var task *entity.Task
|
||||
if err := handler.Model(dao.Task.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(submission.TaskId).Scan(&task); err != nil {
|
||||
return err
|
||||
if submission.TaskId > 0 {
|
||||
var task *entity.Task
|
||||
if err := handler.Model(dao.Task.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(submission.TaskId).Scan(&task); err != nil {
|
||||
return err
|
||||
}
|
||||
if task == nil {
|
||||
return gerror.New("关联任务不存在")
|
||||
}
|
||||
}
|
||||
if task == nil {
|
||||
return gerror.New("关联任务不存在")
|
||||
if submission.TradeOrderId > 0 {
|
||||
oc := dao.TradeOrder.Columns()
|
||||
count, err := handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(submission.TradeOrderId).Where(oc.MemberId, submission.MemberId).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("关联订单不存在或不属于该会员")
|
||||
}
|
||||
}
|
||||
if submission.TaskId == 0 && submission.TradeOrderId == 0 {
|
||||
return gerror.New("提交记录缺少任务和订单关联")
|
||||
}
|
||||
var member *entity.AppMember
|
||||
if err := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(submission.MemberId).Scan(&member); err != nil {
|
||||
|
||||
@@ -56,6 +56,12 @@ func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp)
|
||||
if in.Mobile != "" {
|
||||
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
|
||||
}
|
||||
if in.AutoDispatch == consts.TradeAutoDispatchEnabled {
|
||||
mod = mod.Where("COALESCE(JSON_UNQUOTE(JSON_EXTRACT("+col("o", oc.RuleSnapshot)+",'$.dispatch_mode')),'') = ?", "auto")
|
||||
}
|
||||
if in.AutoDispatch == consts.TradeAutoDispatchDisabled {
|
||||
mod = mod.Where("COALESCE(JSON_UNQUOTE(JSON_EXTRACT("+col("o", oc.RuleSnapshot)+",'$.dispatch_mode')),'') = ?", "manual")
|
||||
}
|
||||
if in.OrderStatus > 0 {
|
||||
mod = mod.Where(col("o", oc.OrderStatus), in.OrderStatus)
|
||||
}
|
||||
@@ -92,7 +98,27 @@ func (s *sSysTradeOrder) View(ctx context.Context, in *sysin.TradeOrderViewInp)
|
||||
if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &sysin.TradeOrderViewModel{TradeOrderListModel: *base, TaskId: order.TaskId, ProductId: order.ProductId, MemberLevelId: order.MemberLevelId, RuleSnapshot: order.RuleSnapshot}
|
||||
res = &sysin.TradeOrderViewModel{TradeOrderListModel: *base, TaskId: order.TaskId, ProductId: order.ProductId, MemberLevelId: order.MemberLevelId, RuleSnapshot: order.RuleSnapshot, UpdatedAt: order.UpdatedAt}
|
||||
res.Timeline = []*sysin.TradeOrderTimelineModel{{Title: "订单创建", Time: order.CreatedAt}}
|
||||
if order.MatchedAt != nil {
|
||||
res.Timeline = append(res.Timeline, &sysin.TradeOrderTimelineModel{Title: "订单匹配", Time: order.MatchedAt})
|
||||
}
|
||||
if order.CompletedAt != nil {
|
||||
res.Timeline = append(res.Timeline, &sysin.TradeOrderTimelineModel{Title: "订单完成", Time: order.CompletedAt})
|
||||
}
|
||||
if order.UnfrozenAt != nil {
|
||||
res.Timeline = append(res.Timeline, &sysin.TradeOrderTimelineModel{Title: "订单解冻", Time: order.UnfrozenAt})
|
||||
}
|
||||
lc := dao.WalletLedger.Columns()
|
||||
if err = handler.Model(dao.WalletLedger.Ctx(ctx), &handler.Option{FilterTenant: true}).Fields(lc.LedgerNo, lc.BizType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Remark, lc.OccurredAt).Where(lc.BizNo, order.OrderNo).OrderAsc(lc.Id).Scan(&res.Ledgers); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取订单资金流水失败")
|
||||
}
|
||||
if order.ApprovalRequestId > 0 {
|
||||
ac := dao.ApprovalAction.Columns()
|
||||
if err = handler.Model(dao.ApprovalAction.Ctx(ctx), &handler.Option{FilterTenant: true}).Fields(ac.StepNo, ac.ActorId, ac.Action, ac.Comment, ac.CreatedAt).Where(ac.ApprovalRequestId, order.ApprovalRequestId).OrderAsc(ac.StepNo).OrderAsc(ac.Id).Scan(&res.ApprovalActions); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取订单审批记录失败")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -11,27 +11,35 @@ import (
|
||||
|
||||
// AppMemberPaymentAccount is the golang structure of table hg_app_member_payment_account for DAO operations like Where/Data.
|
||||
type AppMemberPaymentAccount struct {
|
||||
g.Meta `orm:"table:hg_app_member_payment_account, do:true"`
|
||||
Id any // 主键
|
||||
TenantId any // 租户ID
|
||||
MemberId any // 会员ID
|
||||
AccountType any // 账户类型
|
||||
BankCode any // 银行编码
|
||||
BankName any // 银行名称
|
||||
HolderCipher any // 持有人密文
|
||||
HolderMask any // 持有人脱敏值
|
||||
MobileCipher any // 预留手机号密文
|
||||
MobileMask any // 预留手机号脱敏值
|
||||
AccountNoCipher any // 账号密文
|
||||
AccountNoHash any // 账号检索哈希
|
||||
AccountNoMask any // 账号脱敏值
|
||||
QrAttachmentId any // 收款码附件ID
|
||||
VerifyStatus any // 核验状态
|
||||
IsDefault any // 默认账户
|
||||
CreatedBy any // 创建者
|
||||
UpdatedBy any // 更新者
|
||||
DeletedBy any // 删除者
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
UpdatedAt *gtime.Time // 更新时间
|
||||
DeletedAt *gtime.Time // 删除时间
|
||||
g.Meta `orm:"table:hg_app_member_payment_account, do:true"`
|
||||
Id any // 主键
|
||||
TenantId any // 租户ID
|
||||
MemberId any // 会员ID
|
||||
AccountType any // 账户类型
|
||||
BankCode any // 银行编码
|
||||
BankName any // 银行名称
|
||||
HolderCipher any // 持有人密文
|
||||
HolderMask any // 持有人脱敏值
|
||||
IdentityNoCipher any // 身份证号密文
|
||||
IdentityNoHash any // 身份证号检索哈希
|
||||
IdentityNoMask any // 身份证号脱敏值
|
||||
MobileCipher any // 预留手机号密文
|
||||
MobileMask any // 预留手机号脱敏值
|
||||
AccountNoCipher any // 账号密文
|
||||
AccountNoHash any // 账号检索哈希
|
||||
AccountNoMask any // 账号脱敏值
|
||||
DigitalRmbCodeCipher any // 数字人民币编码密文
|
||||
DigitalRmbCodeHash any // 数字人民币编码检索哈希
|
||||
DigitalRmbCodeMask any // 数字人民币编码脱敏值
|
||||
QrAttachmentId any // 收款码附件ID
|
||||
WechatQrAttachmentId any // 微信二维码附件ID
|
||||
AlipayQrAttachmentId any // 支付宝二维码附件ID
|
||||
VerifyStatus any // 核验状态
|
||||
IsDefault any // 默认账户
|
||||
CreatedBy any // 创建者
|
||||
UpdatedBy any // 更新者
|
||||
DeletedBy any // 删除者
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
UpdatedAt *gtime.Time // 更新时间
|
||||
DeletedAt *gtime.Time // 删除时间
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ type Task struct {
|
||||
TenantId any // 租户ID
|
||||
TaskNo any // 任务编号
|
||||
HallId any // 任务大厅ID
|
||||
ProductId any // 关联商品ID
|
||||
Title any // 标题
|
||||
Description any // 描述
|
||||
DetailHtml any // 清洗后的任务详情
|
||||
|
||||
@@ -10,26 +10,34 @@ import (
|
||||
|
||||
// AppMemberPaymentAccount is the golang structure for table app_member_payment_account.
|
||||
type AppMemberPaymentAccount struct {
|
||||
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
|
||||
MemberId uint64 `json:"memberId" orm:"member_id" description:"会员ID"`
|
||||
AccountType int `json:"accountType" orm:"account_type" description:"账户类型"`
|
||||
BankCode string `json:"bankCode" orm:"bank_code" description:"银行编码"`
|
||||
BankName string `json:"bankName" orm:"bank_name" description:"银行名称"`
|
||||
HolderCipher string `json:"holderCipher" orm:"holder_cipher" description:"持有人密文"`
|
||||
HolderMask string `json:"holderMask" orm:"holder_mask" description:"持有人脱敏值"`
|
||||
MobileCipher string `json:"mobileCipher" orm:"mobile_cipher" description:"预留手机号密文"`
|
||||
MobileMask string `json:"mobileMask" orm:"mobile_mask" description:"预留手机号脱敏值"`
|
||||
AccountNoCipher string `json:"accountNoCipher" orm:"account_no_cipher" description:"账号密文"`
|
||||
AccountNoHash string `json:"accountNoHash" orm:"account_no_hash" description:"账号检索哈希"`
|
||||
AccountNoMask string `json:"accountNoMask" orm:"account_no_mask" description:"账号脱敏值"`
|
||||
QrAttachmentId uint64 `json:"qrAttachmentId" orm:"qr_attachment_id" description:"收款码附件ID"`
|
||||
VerifyStatus int `json:"verifyStatus" orm:"verify_status" description:"核验状态"`
|
||||
IsDefault int `json:"isDefault" orm:"is_default" description:"默认账户"`
|
||||
CreatedBy uint64 `json:"createdBy" orm:"created_by" description:"创建者"`
|
||||
UpdatedBy uint64 `json:"updatedBy" orm:"updated_by" description:"更新者"`
|
||||
DeletedBy uint64 `json:"deletedBy" orm:"deleted_by" description:"删除者"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"删除时间"`
|
||||
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
|
||||
MemberId uint64 `json:"memberId" orm:"member_id" description:"会员ID"`
|
||||
AccountType int `json:"accountType" orm:"account_type" description:"账户类型"`
|
||||
BankCode string `json:"bankCode" orm:"bank_code" description:"银行编码"`
|
||||
BankName string `json:"bankName" orm:"bank_name" description:"银行名称"`
|
||||
HolderCipher string `json:"holderCipher" orm:"holder_cipher" description:"持有人密文"`
|
||||
HolderMask string `json:"holderMask" orm:"holder_mask" description:"持有人脱敏值"`
|
||||
IdentityNoCipher string `json:"identityNoCipher" orm:"identity_no_cipher" description:"身份证号密文"`
|
||||
IdentityNoHash string `json:"identityNoHash" orm:"identity_no_hash" description:"身份证号检索哈希"`
|
||||
IdentityNoMask string `json:"identityNoMask" orm:"identity_no_mask" description:"身份证号脱敏值"`
|
||||
MobileCipher string `json:"mobileCipher" orm:"mobile_cipher" description:"预留手机号密文"`
|
||||
MobileMask string `json:"mobileMask" orm:"mobile_mask" description:"预留手机号脱敏值"`
|
||||
AccountNoCipher string `json:"accountNoCipher" orm:"account_no_cipher" description:"账号密文"`
|
||||
AccountNoHash string `json:"accountNoHash" orm:"account_no_hash" description:"账号检索哈希"`
|
||||
AccountNoMask string `json:"accountNoMask" orm:"account_no_mask" description:"账号脱敏值"`
|
||||
DigitalRmbCodeCipher string `json:"digitalRmbCodeCipher" orm:"digital_rmb_code_cipher" description:"数字人民币编码密文"`
|
||||
DigitalRmbCodeHash string `json:"digitalRmbCodeHash" orm:"digital_rmb_code_hash" description:"数字人民币编码检索哈希"`
|
||||
DigitalRmbCodeMask string `json:"digitalRmbCodeMask" orm:"digital_rmb_code_mask" description:"数字人民币编码脱敏值"`
|
||||
QrAttachmentId uint64 `json:"qrAttachmentId" orm:"qr_attachment_id" description:"收款码附件ID"`
|
||||
WechatQrAttachmentId uint64 `json:"wechatQrAttachmentId" orm:"wechat_qr_attachment_id" description:"微信二维码附件ID"`
|
||||
AlipayQrAttachmentId uint64 `json:"alipayQrAttachmentId" orm:"alipay_qr_attachment_id" description:"支付宝二维码附件ID"`
|
||||
VerifyStatus int `json:"verifyStatus" orm:"verify_status" description:"核验状态"`
|
||||
IsDefault int `json:"isDefault" orm:"is_default" description:"默认账户"`
|
||||
CreatedBy uint64 `json:"createdBy" orm:"created_by" description:"创建者"`
|
||||
UpdatedBy uint64 `json:"updatedBy" orm:"updated_by" description:"更新者"`
|
||||
DeletedBy uint64 `json:"deletedBy" orm:"deleted_by" description:"删除者"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"删除时间"`
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type Task struct {
|
||||
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
|
||||
TaskNo string `json:"taskNo" orm:"task_no" description:"任务编号"`
|
||||
HallId uint64 `json:"hallId" orm:"hall_id" description:"任务大厅ID"`
|
||||
ProductId uint64 `json:"productId" orm:"product_id" description:"关联商品ID"`
|
||||
Title string `json:"title" orm:"title" description:"标题"`
|
||||
Description string `json:"description" orm:"description" description:"描述"`
|
||||
DetailHtml string `json:"detailHtml" orm:"detail_html" description:"清洗后的任务详情"`
|
||||
|
||||
@@ -164,32 +164,44 @@ type AppMemberPaymentListInp struct {
|
||||
func (in *AppMemberPaymentListInp) Filter(ctx context.Context) error { return nil }
|
||||
|
||||
type AppMemberPaymentModel struct {
|
||||
Id uint64 `json:"id"`
|
||||
MemberId uint64 `json:"memberId"`
|
||||
AccountType int `json:"accountType"`
|
||||
BankCode string `json:"bankCode"`
|
||||
BankName string `json:"bankName"`
|
||||
HolderMask string `json:"holderMask"`
|
||||
MobileMask string `json:"mobileMask"`
|
||||
AccountNoMask string `json:"accountNoMask"`
|
||||
QrAttachmentId uint64 `json:"qrAttachmentId"`
|
||||
VerifyStatus int `json:"verifyStatus"`
|
||||
IsDefault int `json:"isDefault"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||
Id uint64 `json:"id"`
|
||||
MemberId uint64 `json:"memberId"`
|
||||
AccountType int `json:"accountType"`
|
||||
BankCode string `json:"bankCode"`
|
||||
BankName string `json:"bankName"`
|
||||
HolderMask string `json:"holderMask"`
|
||||
IdentityNoMask string `json:"identityNoMask"`
|
||||
MobileMask string `json:"mobileMask"`
|
||||
AccountNoMask string `json:"accountNoMask"`
|
||||
DigitalRMBCodeMask string `json:"digitalRmbCodeMask"`
|
||||
QrAttachmentId uint64 `json:"qrAttachmentId"`
|
||||
WechatQrAttachmentId uint64 `json:"wechatQrAttachmentId"`
|
||||
AlipayQrAttachmentId uint64 `json:"alipayQrAttachmentId"`
|
||||
WechatQrUrl string `json:"wechatQrUrl"`
|
||||
AlipayQrUrl string `json:"alipayQrUrl"`
|
||||
VerifyStatus int `json:"verifyStatus"`
|
||||
IsDefault int `json:"isDefault"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||
}
|
||||
type AppMemberPaymentEditInp struct {
|
||||
Id uint64 `json:"id"`
|
||||
MemberId uint64 `json:"memberId" v:"required|min:1#会员ID不能为空|会员ID不能为空"`
|
||||
AccountType int `json:"accountType" v:"required#账户类型不能为空"`
|
||||
BankCode string `json:"bankCode" v:"max-length:32#银行编码不能超过32个字符"`
|
||||
BankName string `json:"bankName" v:"max-length:100#银行名称不能超过100个字符"`
|
||||
Holder string `json:"holder"`
|
||||
Mobile string `json:"mobile" v:"phone-loose#手机号格式不正确"`
|
||||
AccountNo string `json:"accountNo"`
|
||||
QrAttachmentId uint64 `json:"qrAttachmentId"`
|
||||
VerifyStatus int `json:"verifyStatus" d:"1"`
|
||||
IsDefault int `json:"isDefault" d:"2"`
|
||||
Id uint64 `json:"id"`
|
||||
MemberId uint64 `json:"memberId" v:"required|min:1#会员ID不能为空|会员ID不能为空"`
|
||||
AccountType int `json:"accountType" v:"required#账户类型不能为空"`
|
||||
BankCode string `json:"bankCode" v:"max-length:32#银行编码不能超过32个字符"`
|
||||
BankName string `json:"bankName" v:"max-length:100#银行名称不能超过100个字符"`
|
||||
Holder string `json:"holder"`
|
||||
IdentityNo string `json:"identityNo"`
|
||||
Mobile string `json:"mobile" v:"phone-loose#手机号格式不正确"`
|
||||
AccountNo string `json:"accountNo"`
|
||||
DigitalRMBCode string `json:"digitalRmbCode"`
|
||||
QrAttachmentId uint64 `json:"qrAttachmentId"`
|
||||
WechatQrAttachmentId uint64 `json:"wechatQrAttachmentId"`
|
||||
AlipayQrAttachmentId uint64 `json:"alipayQrAttachmentId"`
|
||||
WechatQrUrl string `json:"wechatQrUrl"`
|
||||
AlipayQrUrl string `json:"alipayQrUrl"`
|
||||
VerifyStatus int `json:"verifyStatus" d:"1"`
|
||||
IsDefault int `json:"isDefault" d:"2"`
|
||||
}
|
||||
|
||||
func (in *AppMemberPaymentEditInp) Filter(ctx context.Context) error {
|
||||
|
||||
@@ -55,14 +55,12 @@ func (in *SiteConfigSaveInp) Filter(ctx context.Context) error {
|
||||
if strings.TrimSpace(in.Config.SiteName) == "" {
|
||||
return gerror.New("站点名称不能为空")
|
||||
}
|
||||
if in.Config.PrivacyPolicyUrl != "" {
|
||||
u, err := url.Parse(in.Config.PrivacyPolicyUrl)
|
||||
if err != nil || u.Scheme != "https" || u.Host == "" {
|
||||
return gerror.New("隐私政策地址必须是有效的 HTTPS 地址")
|
||||
}
|
||||
u, err := url.Parse(in.Config.PrivacyPolicyUrl)
|
||||
if err != nil || u.Scheme != "https" || u.Host == "" {
|
||||
return gerror.New("隐私政策地址不能为空且必须是有效的 HTTPS 地址")
|
||||
}
|
||||
if in.Config.TradeWaitSeconds < 0 || in.Config.DailyTradeLimit < 0 || in.Config.DailyRewardLimit < 0 {
|
||||
return gerror.New("交易等待时间和每日次数不能为负数")
|
||||
if in.Config.TradeBalanceAmount <= 0 || in.Config.TradeWaitSeconds <= 0 || in.Config.DailyTradeLimit <= 0 || in.Config.DailyRewardLimit <= 0 || in.Config.MinimumRechargeAmount <= 0 {
|
||||
return gerror.New("交易余额、支付等待时长、每日次数和最低充值金额必须大于0")
|
||||
}
|
||||
if len(in.Config.CommissionRates) != 6 {
|
||||
return gerror.New("必须配置 0 至 5 级共 6 个佣金比例")
|
||||
@@ -77,11 +75,11 @@ func (in *SiteConfigSaveInp) Filter(ctx context.Context) error {
|
||||
if total > 1 {
|
||||
return gerror.New("各级佣金比例总和不能超过 100%")
|
||||
}
|
||||
if in.Config.AutoOrderDelaySeconds < 0 {
|
||||
return gerror.New("自动派单时间不能为负数")
|
||||
if in.Config.AutoOrderDelaySeconds <= 0 {
|
||||
return gerror.New("自动派单时间必须大于0")
|
||||
}
|
||||
if in.Config.AutoOrderMinAmount < 0 || in.Config.AutoOrderMaxAmount < in.Config.AutoOrderMinAmount {
|
||||
return gerror.New("自动派单最大金额不能小于最小金额")
|
||||
if in.Config.AutoOrderMinAmount <= 0 || in.Config.AutoOrderMaxAmount < in.Config.AutoOrderMinAmount {
|
||||
return gerror.New("自动派单金额必须大于0,且最大金额不能小于最小金额")
|
||||
}
|
||||
for _, ranges := range [][]string{in.Config.WithdrawTimeRanges, in.Config.RechargeTimeRanges, in.Config.TradeTimeRanges, in.Config.SignTimeRanges} {
|
||||
for _, value := range ranges {
|
||||
|
||||
@@ -123,6 +123,7 @@ func (in *ProductDeleteInp) Filter(ctx context.Context) error { return nil }
|
||||
|
||||
type TaskUpdateFields struct {
|
||||
HallId uint64
|
||||
ProductId uint64
|
||||
Title string
|
||||
Description string
|
||||
DetailHtml string
|
||||
@@ -157,10 +158,11 @@ func (in *TaskListInp) Filter(ctx context.Context) error { return nil }
|
||||
|
||||
type TaskListModel struct {
|
||||
entity.Task
|
||||
HallName string `json:"hallName"`
|
||||
LevelIds []uint64 `json:"levelIds"`
|
||||
LevelNames []string `json:"levelNames"`
|
||||
Referenced bool `json:"referenced"`
|
||||
HallName string `json:"hallName"`
|
||||
ProductName string `json:"productName"`
|
||||
LevelIds []uint64 `json:"levelIds"`
|
||||
LevelNames []string `json:"levelNames"`
|
||||
Referenced bool `json:"referenced"`
|
||||
}
|
||||
type TaskViewInp struct {
|
||||
Id uint64 `json:"id" v:"required|min:1#任务ID不能为空|任务ID不能为空"`
|
||||
@@ -178,8 +180,8 @@ func (in *TaskEditInp) Filter(ctx context.Context) error {
|
||||
if strings.TrimSpace(in.Title) == "" || strings.TrimSpace(in.Description) == "" || strings.TrimSpace(in.CoverUrl) == "" {
|
||||
return gerror.New("任务标题、描述和封面不能为空")
|
||||
}
|
||||
if in.HallId == 0 || len(in.LevelIds) == 0 {
|
||||
return gerror.New("请选择任务大厅和会员等级")
|
||||
if in.HallId == 0 || in.ProductId == 0 || len(in.LevelIds) == 0 {
|
||||
return gerror.New("请选择关联商品、任务大厅和会员等级")
|
||||
}
|
||||
if in.PerMemberLimit == 0 {
|
||||
return gerror.New("单会员任务限制必须大于0")
|
||||
@@ -231,6 +233,8 @@ type TaskSubmissionListModel struct {
|
||||
Id uint64 `json:"id"`
|
||||
SubmissionNo string `json:"submissionNo"`
|
||||
TradeOrderId uint64 `json:"tradeOrderId"`
|
||||
TradeOrderNo string `json:"tradeOrderNo"`
|
||||
ProductName string `json:"productName"`
|
||||
TaskId uint64 `json:"taskId"`
|
||||
TaskTitle string `json:"taskTitle"`
|
||||
MemberId uint64 `json:"memberId"`
|
||||
|
||||
@@ -16,11 +16,12 @@ import (
|
||||
|
||||
type TradeOrderListInp struct {
|
||||
form.PageReq
|
||||
OrderNo string `json:"orderNo"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
OrderStatus int `json:"orderStatus"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt"`
|
||||
OrderNo string `json:"orderNo"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
AutoDispatch int `json:"autoDispatch"`
|
||||
OrderStatus int `json:"orderStatus"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (in *TradeOrderListInp) Filter(ctx context.Context) error { return nil }
|
||||
@@ -70,10 +71,36 @@ func (in *TradeOrderViewInp) Filter(ctx context.Context) error { return nil }
|
||||
|
||||
type TradeOrderViewModel struct {
|
||||
TradeOrderListModel
|
||||
TaskId uint64 `json:"taskId"`
|
||||
ProductId uint64 `json:"productId"`
|
||||
MemberLevelId uint64 `json:"memberLevelId"`
|
||||
RuleSnapshot *gjson.Json `json:"ruleSnapshot"`
|
||||
TaskId uint64 `json:"taskId"`
|
||||
ProductId uint64 `json:"productId"`
|
||||
MemberLevelId uint64 `json:"memberLevelId"`
|
||||
RuleSnapshot *gjson.Json `json:"ruleSnapshot"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||
Timeline []*TradeOrderTimelineModel `json:"timeline"`
|
||||
Ledgers []*TradeOrderLedgerModel `json:"ledgers"`
|
||||
ApprovalActions []*TradeOrderApprovalActionModel `json:"approvalActions"`
|
||||
}
|
||||
|
||||
type TradeOrderTimelineModel struct {
|
||||
Title string `json:"title"`
|
||||
Time *gtime.Time `json:"time"`
|
||||
}
|
||||
type TradeOrderLedgerModel struct {
|
||||
LedgerNo string `json:"ledgerNo"`
|
||||
BizType string `json:"bizType"`
|
||||
Direction int `json:"direction"`
|
||||
Amount int64 `json:"amount"`
|
||||
BeforeAmount int64 `json:"beforeAmount"`
|
||||
AfterAmount int64 `json:"afterAmount"`
|
||||
Remark string `json:"remark"`
|
||||
OccurredAt *gtime.Time `json:"occurredAt"`
|
||||
}
|
||||
type TradeOrderApprovalActionModel struct {
|
||||
StepNo uint `json:"stepNo"`
|
||||
ActorId uint64 `json:"actorId"`
|
||||
Action int `json:"action"`
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type TradeOrderApplyInp struct {
|
||||
|
||||
@@ -167,6 +167,7 @@ type (
|
||||
}
|
||||
ISysDashboard interface {
|
||||
Summary(ctx context.Context, in *sysin.DashboardSummaryInp) (res *sysin.DashboardSummaryModel, err error)
|
||||
RefreshDaily(ctx context.Context, days int) (err error)
|
||||
}
|
||||
ISysBizAudit interface {
|
||||
Model(ctx context.Context, option ...*handler.Option) *gdb.Model
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 360" role="img" aria-label="Olons 平台横幅">
|
||||
<defs><linearGradient id="g" x1="0" x2="1"><stop stop-color="#1ba8ee"/><stop offset="1" stop-color="#3136dc"/></linearGradient></defs>
|
||||
<rect width="1200" height="360" rx="28" fill="url(#g)"/>
|
||||
<circle cx="1010" cy="70" r="180" fill="#fff" opacity=".10"/><circle cx="1120" cy="320" r="220" fill="#fff" opacity=".08"/>
|
||||
<text x="90" y="160" fill="#fff" font-size="66" font-family="Arial, sans-serif" font-weight="700">OLONS</text>
|
||||
<text x="92" y="230" fill="#fff" font-size="34" font-family="Arial, sans-serif" opacity=".9">精选商品 · 安心服务</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" role="img" aria-label="Olons 商品占位图">
|
||||
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#eaf7ff"/><stop offset="1" stop-color="#dfe3ff"/></linearGradient></defs>
|
||||
<rect width="800" height="800" rx="48" fill="url(#g)"/>
|
||||
<path d="M220 300h360l-28 300H248z" fill="#fff" stroke="#2878e8" stroke-width="22"/>
|
||||
<path d="M310 315c0-118 180-118 180 0" fill="none" stroke="#2878e8" stroke-width="22" stroke-linecap="round"/>
|
||||
<text x="400" y="690" text-anchor="middle" fill="#2878e8" font-size="54" font-family="Arial, sans-serif" font-weight="700">OLONS</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 663 B |
@@ -0,0 +1,15 @@
|
||||
-- 任务明确关联一个已发布商品,便于创建、审核和订单追溯。
|
||||
ALTER TABLE `hg_task`
|
||||
ADD COLUMN `product_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联商品ID' AFTER `hall_id`,
|
||||
ADD INDEX `idx_task_product_id` (`product_id`);
|
||||
|
||||
-- 历史任务从已生成的交易订单中回填最常用的商品,无法判断的保持 0 并在后台补录。
|
||||
UPDATE `hg_task` t
|
||||
LEFT JOIN (
|
||||
SELECT `task_id`, MAX(`product_id`) AS `product_id`
|
||||
FROM `hg_trade_order`
|
||||
WHERE `task_id` > 0 AND `product_id` > 0
|
||||
GROUP BY `task_id`
|
||||
) o ON o.`task_id` = t.`id`
|
||||
SET t.`product_id` = COALESCE(o.`product_id`, 0)
|
||||
WHERE t.`product_id` = 0;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 使用 HotGo 自带定时任务管理,每 10 分钟重算今天和昨天的经营看板。
|
||||
INSERT INTO `hg_sys_cron`
|
||||
(`group_id`,`title`,`name`,`params`,`pattern`,`policy`,`count`,`sort`,`remark`,`status`,`created_at`,`updated_at`)
|
||||
SELECT 1,'经营看板日汇总','dashboard_daily_summary','2','0 */10 * * * *',2,1,20,
|
||||
'汇总会员、商品、订单、充值、提现和钱包数据;参数为重算天数',1,NOW(),NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_sys_cron` WHERE `name`='dashboard_daily_summary');
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 补全历史站点配置中的无效空值;金额单位均为分。
|
||||
UPDATE `hg_site_config_version`
|
||||
SET `config_json` = JSON_SET(
|
||||
`config_json`,
|
||||
'$.siteName', IF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName')),'')='' OR JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName')) LIKE '演示站点%', 'Olons', JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.siteName'))),
|
||||
'$.privacyPolicyUrl', IF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.privacyPolicyUrl')),'')='', 'https://olns-web.118im.com/admin/#/app/content/privacy_policy', JSON_UNQUOTE(JSON_EXTRACT(`config_json`,'$.privacyPolicyUrl'))),
|
||||
'$.tradeBalanceAmount', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.tradeBalanceAmount'),0)<=0,10000,JSON_EXTRACT(`config_json`,'$.tradeBalanceAmount')),
|
||||
'$.tradeWaitSeconds', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.tradeWaitSeconds'),0)<=0,1200,JSON_EXTRACT(`config_json`,'$.tradeWaitSeconds')),
|
||||
'$.dailyTradeLimit', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.dailyTradeLimit'),0)<=0,30,JSON_EXTRACT(`config_json`,'$.dailyTradeLimit')),
|
||||
'$.dailyRewardLimit', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.dailyRewardLimit'),0)<=0,3,JSON_EXTRACT(`config_json`,'$.dailyRewardLimit')),
|
||||
'$.minimumRechargeAmount', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.minimumRechargeAmount'),0)<=0,10000,JSON_EXTRACT(`config_json`,'$.minimumRechargeAmount')),
|
||||
'$.autoOrderDelaySeconds', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderDelaySeconds'),0)<=0,10,JSON_EXTRACT(`config_json`,'$.autoOrderDelaySeconds')),
|
||||
'$.autoOrderMinAmount', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderMinAmount'),0)<=0,100,JSON_EXTRACT(`config_json`,'$.autoOrderMinAmount')),
|
||||
'$.autoOrderMaxAmount', IF(COALESCE(JSON_EXTRACT(`config_json`,'$.autoOrderMaxAmount'),0)<=0,10000000,JSON_EXTRACT(`config_json`,'$.autoOrderMaxAmount')),
|
||||
'$.commissionRates', IF(JSON_LENGTH(COALESCE(JSON_EXTRACT(`config_json`,'$.commissionRates'),JSON_ARRAY()))<>6,JSON_ARRAY(0.10,0.05,0.03,0.02,0.01,0),JSON_EXTRACT(`config_json`,'$.commissionRates'))
|
||||
), `updated_at`=NOW()
|
||||
WHERE `config_group`='portal';
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 为历史空图片记录提供可随服务部署的本地占位资源,后续运营可正常替换。
|
||||
UPDATE `hg_banner`
|
||||
SET `image_url`='/images/olons-banner.svg', `updated_at`=NOW()
|
||||
WHERE `deleted_at` IS NULL AND COALESCE(`image_url`,'')='';
|
||||
|
||||
UPDATE `hg_product`
|
||||
SET `cover_url`='/images/olons-product.svg', `updated_at`=NOW()
|
||||
WHERE `deleted_at` IS NULL AND COALESCE(`cover_url`,'')='';
|
||||
@@ -175,23 +175,32 @@
|
||||
<n-spin :show="relatedLoading">
|
||||
<n-form label-placement="left" label-width="110">
|
||||
<template v-if="relatedType === 'payment'">
|
||||
<n-alert v-if="paymentForm.id" type="info" :show-icon="false">
|
||||
当前默认显示脱敏资料;留空保存不会覆盖原敏感字段或收款码。
|
||||
<n-button
|
||||
v-if="hasPermission(['/appMember/sensitive'])"
|
||||
text
|
||||
type="primary"
|
||||
@click="openSensitive('payment', paymentForm.id)"
|
||||
>授权查看明文</n-button>
|
||||
</n-alert>
|
||||
<n-form-item label="开户人"
|
||||
><n-input v-model:value="paymentForm.holder" placeholder="请输入开户人"
|
||||
><n-input v-model:value="paymentForm.holder" :placeholder="paymentForm.holderMask || '留空不修改'"
|
||||
/></n-form-item>
|
||||
<n-form-item label="身份证号"
|
||||
><n-input v-model:value="paymentForm.identityNo" :placeholder="paymentForm.identityNoMask || '留空不修改'"
|
||||
/></n-form-item>
|
||||
<n-form-item label="开户银行"
|
||||
><n-input v-model:value="paymentForm.bankName" placeholder="请输入开户银行"
|
||||
/></n-form-item>
|
||||
<n-form-item label="开户电话"
|
||||
><n-input v-model:value="paymentForm.mobile" placeholder="请输入开户电话"
|
||||
><n-input v-model:value="paymentForm.mobile" :placeholder="paymentForm.mobileMask || '留空不修改'"
|
||||
/></n-form-item>
|
||||
<n-form-item label="银行卡号"
|
||||
><n-input v-model:value="paymentForm.bankAccount" placeholder="请输入银行卡号"
|
||||
><n-input v-model:value="paymentForm.bankAccount" :placeholder="paymentForm.accountNoMask || '留空不修改'"
|
||||
/></n-form-item>
|
||||
<n-form-item label="数字钱包"
|
||||
><n-input v-model:value="paymentForm.digitalWallet" placeholder="请输入数字钱包"
|
||||
/></n-form-item>
|
||||
<n-form-item label="云闪付账号"
|
||||
><n-input v-model:value="paymentForm.unionPayAccount" placeholder="请输入云闪付账号"
|
||||
><n-input v-model:value="paymentForm.digitalWallet" :placeholder="paymentForm.digitalRmbCodeMask || '留空不修改'"
|
||||
/></n-form-item>
|
||||
<n-form-item label="收款码">
|
||||
<n-grid :cols="2" :x-gap="16">
|
||||
@@ -743,14 +752,20 @@
|
||||
memberId,
|
||||
id: bank?.id || 0,
|
||||
holder: '',
|
||||
holderMask: bank?.holderMask || '',
|
||||
identityNo: '',
|
||||
identityNoMask: bank?.identityNoMask || '',
|
||||
bankName: bank?.bankName || '',
|
||||
mobile: '',
|
||||
mobileMask: bank?.mobileMask || '',
|
||||
bankAccount: '',
|
||||
accountNoMask: bank?.accountNoMask || '',
|
||||
digitalWallet: '',
|
||||
unionPayAccount: '',
|
||||
alipayQrCode: '',
|
||||
wechatQrCode: '',
|
||||
records: { 1: bank, 2: byType(2), 3: byType(3), 4: byType(4), 5: byType(5) },
|
||||
digitalRmbCodeMask: bank?.digitalRmbCodeMask || '',
|
||||
alipayQrCode: bank?.alipayQrUrl || '',
|
||||
wechatQrCode: bank?.wechatQrUrl || '',
|
||||
alipayQrAttachmentId: bank?.alipayQrAttachmentId || 0,
|
||||
wechatQrAttachmentId: bank?.wechatQrAttachmentId || 0,
|
||||
});
|
||||
} else {
|
||||
const row = relatedRows.value.find((item) => item.isDefault === 1) || relatedRows.value[0];
|
||||
@@ -811,32 +826,23 @@
|
||||
relatedSaving.value = true;
|
||||
try {
|
||||
if (relatedType.value === 'payment') {
|
||||
const common = {
|
||||
await PaymentEdit({
|
||||
id: paymentForm.id,
|
||||
memberId: paymentForm.memberId,
|
||||
accountType: 1,
|
||||
bankName: paymentForm.bankName,
|
||||
holder: paymentForm.holder,
|
||||
identityNo: paymentForm.identityNo,
|
||||
mobile: paymentForm.mobile,
|
||||
accountNo: paymentForm.bankAccount,
|
||||
digitalRmbCode: paymentForm.digitalWallet,
|
||||
alipayQrUrl: paymentForm.alipayQrCode,
|
||||
wechatQrUrl: paymentForm.wechatQrCode,
|
||||
alipayQrAttachmentId: paymentForm.alipayQrAttachmentId,
|
||||
wechatQrAttachmentId: paymentForm.wechatQrAttachmentId,
|
||||
verifyStatus: 1,
|
||||
isDefault: 1,
|
||||
};
|
||||
const accounts = [
|
||||
{ type: 1, value: paymentForm.bankAccount, bankName: paymentForm.bankName },
|
||||
{ type: 2, value: paymentForm.digitalWallet, bankName: '' },
|
||||
{ type: 3, value: paymentForm.unionPayAccount, bankName: '' },
|
||||
{ type: 4, value: paymentForm.alipayQrCode, bankName: '' },
|
||||
{ type: 5, value: paymentForm.wechatQrCode, bankName: '' },
|
||||
];
|
||||
for (const account of accounts) {
|
||||
const old = paymentForm.records?.[account.type];
|
||||
if (account.value || old?.id)
|
||||
await PaymentEdit({
|
||||
...common,
|
||||
id: old?.id || 0,
|
||||
accountType: account.type,
|
||||
accountNo: account.value,
|
||||
bankName: account.bankName,
|
||||
isDefault: account.type === 1 ? 1 : 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const regionChanged = addressForm.region !== addressForm.originalRegion;
|
||||
await AddressEdit({
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { Delete, Edit, List, View } from '@/api/taskHall';
|
||||
import { Delete, Edit, List, Status, View } from '@/api/taskHall';
|
||||
import { columns, loadOptions } from './model';
|
||||
|
||||
const message = useMessage();
|
||||
@@ -100,7 +100,7 @@
|
||||
};
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 180,
|
||||
width: 260,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
@@ -114,6 +114,12 @@
|
||||
auth: ['/taskHall/view'],
|
||||
onClick: () => openEdit(row),
|
||||
},
|
||||
{
|
||||
label: row.publishStatus === 3 ? '下线' : '发布',
|
||||
type: row.publishStatus === 3 ? 'warning' : 'success',
|
||||
auth: ['/taskHall/status'],
|
||||
onClick: () => changeStatus(row),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'error',
|
||||
@@ -237,6 +243,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
function changeStatus(row) {
|
||||
const publish = row.publishStatus !== 3;
|
||||
dialog.warning({
|
||||
title: publish ? '发布任务大厅' : '下线任务大厅',
|
||||
content: `确定${publish ? '发布' : '下线'}“${row.name}”吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await Status({
|
||||
id: row.id,
|
||||
version: row.version,
|
||||
publishStatus: publish ? 3 : 4,
|
||||
});
|
||||
message.success(`${publish ? '发布' : '下线'}成功`);
|
||||
reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { h } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { renderImage } from '@/utils';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
|
||||
@@ -14,7 +16,18 @@ export const columns = [
|
||||
render: (row) => `${((row.commissionRate || 0) / 100).toFixed(2)}%`,
|
||||
},
|
||||
{ title: '简介', key: 'summary', width: 260, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'publishStatus',
|
||||
width: 100,
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: dict.getType('bizPublishStatus', row.publishStatus), bordered: false },
|
||||
{ default: () => dict.getLabel('bizPublishStatus', row.publishStatus) || '-' }
|
||||
),
|
||||
},
|
||||
{ title: '创建时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
|
||||
export const loadOptions = () => dict.loadOptions(['memberLevelOption']);
|
||||
export const loadOptions = () => dict.loadOptions(['memberLevelOption', 'bizPublishStatus']);
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
</template>
|
||||
</n-result>
|
||||
</n-spin>
|
||||
<template #action>
|
||||
<n-space justify="end">
|
||||
<n-button :disabled="importing" @click="showImport = false">取消</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showDetail" preset="card" title="地址详情" class="detail-modal">
|
||||
|
||||
@@ -37,14 +37,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { h, reactive, ref } from 'vue';
|
||||
import { NImage, useDialog, useMessage } from 'naive-ui';
|
||||
import { h, onMounted, reactive, ref } from 'vue';
|
||||
import { NImage, NTag, useDialog, useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { ManualProductDelete, ManualProductList } from '@/api/portal';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { ManualProductDelete, ManualProductList, ManualProductStatus } from '@/api/portal';
|
||||
import Edit from './edit.vue';
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const dict = useDictStore();
|
||||
const dialog = useDialog();
|
||||
const message = useMessage();
|
||||
const tableRef = ref();
|
||||
@@ -64,12 +66,23 @@
|
||||
},
|
||||
{ title: '产品名称', key: 'name', minWidth: 320 },
|
||||
{ title: '价格', key: 'price', width: 140 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'publishStatus',
|
||||
width: 100,
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: dict.getType('bizPublishStatus', row.publishStatus), bordered: false },
|
||||
{ default: () => dict.getLabel('bizPublishStatus', row.publishStatus) || '-' }
|
||||
),
|
||||
},
|
||||
{ title: '创建时间', key: 'createdAt', width: 190 },
|
||||
];
|
||||
|
||||
const loadTable = (params) => ManualProductList(params);
|
||||
const actionColumn = reactive({
|
||||
width: 180,
|
||||
width: 260,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
@@ -82,6 +95,12 @@
|
||||
auth: ['/manualProduct/edit'],
|
||||
onClick: () => editRef.value.open(row),
|
||||
},
|
||||
{
|
||||
label: row.publishStatus === 3 ? '下架' : '发布',
|
||||
type: row.publishStatus === 3 ? 'warning' : 'success',
|
||||
auth: ['/manualProduct/status'],
|
||||
onClick: () => changeStatus(row),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
auth: ['/manualProduct/delete'],
|
||||
@@ -121,6 +140,27 @@
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function changeStatus(row) {
|
||||
const publish = row.publishStatus !== 3;
|
||||
dialog.warning({
|
||||
title: publish ? '发布商品' : '下架商品',
|
||||
content: `确定${publish ? '发布' : '下架'}商品“${row.name}”吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await ManualProductStatus({
|
||||
id: row.id,
|
||||
version: row.version,
|
||||
publishStatus: publish ? 3 : 4,
|
||||
});
|
||||
message.success(`${publish ? '发布' : '下架'}成功`);
|
||||
reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => dict.loadOptions(['bizPublishStatus']));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<n-form-item label="交易所需余额(必填)">
|
||||
<n-input-number v-model:value="form.tradeBalanceAmount" :min="0" />
|
||||
</n-form-item>
|
||||
<n-form-item label="未订单的支付等待时长(秒)">
|
||||
<n-form-item label="未支付订单等待时长(秒)">
|
||||
<n-input-number v-model:value="form.tradeWaitSeconds" :min="0" />
|
||||
</n-form-item>
|
||||
<n-form-item label="当日交易次数限制">
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<n-spin :show="loading">
|
||||
<n-descriptions v-if="detail.id" bordered :column="2">
|
||||
<n-descriptions-item label="任务">{{ detail.taskTitle }}</n-descriptions-item>
|
||||
<n-descriptions-item label="订单">
|
||||
{{ detail.tradeOrderNo || `#${detail.tradeOrderId || '-'}` }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="商品">{{ detail.productName || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="会员">
|
||||
{{ detail.username }} / {{ detail.mobile }}
|
||||
</n-descriptions-item>
|
||||
|
||||
@@ -35,6 +35,7 @@ export const columns = [
|
||||
{ title: '用户名', key: 'username', width: 130 },
|
||||
{ title: '手机号', key: 'mobile', width: 130 },
|
||||
{ title: '任务标题', key: 'taskTitle', width: 180 },
|
||||
{ title: '订单号', key: 'tradeOrderNo', width: 180, render: (r) => r.tradeOrderNo || `#${r.tradeOrderId || '-'}` },
|
||||
{ title: '等级', key: 'levelName', width: 110 },
|
||||
{
|
||||
title: '佣金',
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
><n-select
|
||||
v-model:value="form.hallId"
|
||||
:options="dict.getOptionUnRef('taskHallOption')" /></n-form-item-gi
|
||||
><n-form-item-gi label="关联商品" path="productId"
|
||||
><n-select
|
||||
v-model:value="form.productId"
|
||||
filterable
|
||||
:loading="productLoading"
|
||||
:options="productOptions"
|
||||
placeholder="请选择关联商品" /></n-form-item-gi
|
||||
><n-form-item-gi label="会员等级" :span="2" path="levelIds"
|
||||
><n-select
|
||||
v-model:value="form.levelIds"
|
||||
@@ -61,7 +68,7 @@
|
||||
import Editor from '@/components/Editor/editor.vue';
|
||||
import UploadImage from '@/components/Upload/uploadImage.vue';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { TaskEdit, TaskView } from '@/api/productTask';
|
||||
import { ProductList, TaskEdit, TaskView } from '@/api/productTask';
|
||||
const emit = defineEmits(['reload']),
|
||||
message = useMessage(),
|
||||
dict = useDictStore(),
|
||||
@@ -70,7 +77,9 @@
|
||||
saving = ref(false),
|
||||
formRef = ref(),
|
||||
dateRange = ref<any>(),
|
||||
commissionPercent = ref(0);
|
||||
commissionPercent = ref(0),
|
||||
productLoading = ref(false),
|
||||
productOptions = ref<any[]>([]);
|
||||
const init = () => ({
|
||||
id: 0,
|
||||
version: 0,
|
||||
@@ -80,6 +89,7 @@
|
||||
coverAttachmentId: 0,
|
||||
coverUrl: '',
|
||||
hallId: null,
|
||||
productId: null,
|
||||
levelIds: [],
|
||||
commission: '0.00',
|
||||
dailyLimit: 0,
|
||||
@@ -93,6 +103,7 @@
|
||||
description: { required: true, message: '请输入任务描述' },
|
||||
coverUrl: { required: true, message: '请上传任务封面' },
|
||||
hallId: { required: true, type: 'number', message: '请选择任务大厅' },
|
||||
productId: { required: true, type: 'number', message: '请选择关联商品' },
|
||||
levelIds: { required: true, type: 'array', min: 1, message: '请选择会员等级' },
|
||||
commission: { required: true, message: '请输入固定佣金' },
|
||||
};
|
||||
@@ -101,6 +112,16 @@
|
||||
commissionPercent.value = 0;
|
||||
dateRange.value = null;
|
||||
show.value = true;
|
||||
productLoading.value = true;
|
||||
try {
|
||||
const result: any = await ProductList({ page: 1, perPage: 500 });
|
||||
productOptions.value = (result.list || []).map((item) => ({
|
||||
label: `${item.name}(${item.productNo})`,
|
||||
value: item.id,
|
||||
}));
|
||||
} finally {
|
||||
productLoading.value = false;
|
||||
}
|
||||
if (row) {
|
||||
loading.value = true;
|
||||
try {
|
||||
|
||||
@@ -1 +1,27 @@
|
||||
import{computed,h,ref}from'vue';import{NImage,NTag}from'naive-ui';import{FormSchema}from'@/components/Form';import{useDictStore}from'@/store/modules/dict';const dict=useDictStore();export const schemas=ref<FormSchema[]>([{field:'title',component:'NInput',label:'任务标题',componentProps:{clearable:true}},{field:'levelId',component:'NSelect',label:'会员类型',componentProps:{options:computed(()=>dict.getOptionUnRef('memberLevelOption')),clearable:true}},{field:'hallId',component:'NSelect',label:'任务大厅',componentProps:{options:computed(()=>dict.getOptionUnRef('taskHallOption')),clearable:true}},{field:'publishStatus',component:'NSelect',label:'状态',componentProps:{options:computed(()=>dict.getOptionUnRef('bizPublishStatus')),clearable:true}}]);const tag=(k,v)=>h(NTag,{type:dict.getType(k,v),bordered:false},()=>dict.getLabel(k,v)||'-');export const columns=[{title:'任务编号',key:'taskNo',width:180},{title:'封面',key:'coverUrl',width:100,render:r=>h(NImage,{src:r.coverUrl,width:64,height:64,objectFit:'cover'})},{title:'标题',key:'title',width:180},{title:'描述',key:'description',width:220,ellipsis:{tooltip:true}},{title:'任务大厅',key:'hallName',width:130},{title:'会员等级',key:'levelNames',width:180,render:r=>(r.levelNames||[]).join('、')},{title:'固定佣金',key:'commissionAmount',width:100,render:r=>`¥${((r.commissionAmount||0)/100).toFixed(2)}`},{title:'比例佣金',key:'commissionRate',width:100,render:r=>`${((r.commissionRate||0)/100).toFixed(2)}%`},{title:'状态',key:'publishStatus',width:100,render:r=>tag('bizPublishStatus',r.publishStatus)},{title:'创建时间',key:'createdAt',width:180}];export const loadOptions=()=>dict.loadOptions(['memberLevelOption','taskHallOption','bizPublishStatus']);
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { NImage, NTag } from 'naive-ui';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
|
||||
const dict = useDictStore();
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{ field: 'title', component: 'NInput', label: '任务标题', componentProps: { clearable: true } },
|
||||
{ field: 'levelId', component: 'NSelect', label: '会员类型', componentProps: { options: computed(() => dict.getOptionUnRef('memberLevelOption')), clearable: true } },
|
||||
{ field: 'hallId', component: 'NSelect', label: '任务大厅', componentProps: { options: computed(() => dict.getOptionUnRef('taskHallOption')), clearable: true } },
|
||||
{ field: 'publishStatus', component: 'NSelect', label: '状态', componentProps: { options: computed(() => dict.getOptionUnRef('bizPublishStatus')), clearable: true } },
|
||||
]);
|
||||
const tag = (key, value) => h(NTag, { type: dict.getType(key, value), bordered: false }, () => dict.getLabel(key, value) || '-');
|
||||
export const columns = [
|
||||
{ title: '任务编号', key: 'taskNo', width: 180 },
|
||||
{ title: '封面', key: 'coverUrl', width: 100, render: (row) => h(NImage, { src: row.coverUrl, width: 64, height: 64, objectFit: 'cover' }) },
|
||||
{ title: '标题', key: 'title', width: 180 },
|
||||
{ title: '关联商品', key: 'productName', width: 180, render: (row) => row.productName || '待补录' },
|
||||
{ title: '描述', key: 'description', width: 220, ellipsis: { tooltip: true } },
|
||||
{ title: '任务大厅', key: 'hallName', width: 130 },
|
||||
{ title: '会员等级', key: 'levelNames', width: 180, render: (row) => (row.levelNames || []).join('、') },
|
||||
{ title: '固定佣金', key: 'commissionAmount', width: 100, render: (row) => `¥${((row.commissionAmount || 0) / 100).toFixed(2)}` },
|
||||
{ title: '比例佣金', key: 'commissionRate', width: 100, render: (row) => `${((row.commissionRate || 0) / 100).toFixed(2)}%` },
|
||||
{ title: '状态', key: 'publishStatus', width: 100, render: (row) => tag('bizPublishStatus', row.publishStatus) },
|
||||
{ title: '创建时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
export const loadOptions = () => dict.loadOptions(['memberLevelOption', 'taskHallOption', 'bizPublishStatus']);
|
||||
|
||||
@@ -68,8 +68,19 @@
|
||||
<n-descriptions-item label="订单号">{{ detail.orderNo }}</n-descriptions-item><n-descriptions-item label="会员">{{ detail.username }} / {{ detail.mobile }}</n-descriptions-item>
|
||||
<n-descriptions-item label="商品/任务">{{ detail.productName || '-' }}</n-descriptions-item><n-descriptions-item label="规则等级ID">{{ detail.memberLevelId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="订单金额">{{ yuan(detail.totalAmount) }}</n-descriptions-item><n-descriptions-item label="佣金">{{ yuan(detail.commissionAmount) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="订单状态">{{ dict.getLabel('tradeOrderStatus', detail.orderStatus) || '-' }}</n-descriptions-item><n-descriptions-item label="冻结状态">{{ dict.getLabel('tradeFreezeStatus', detail.freezeStatus) || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">{{ detail.createdAt || '-' }}</n-descriptions-item><n-descriptions-item label="更新时间">{{ detail.updatedAt || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="规则快照" :span="2"><n-code :code="JSON.stringify(detail.ruleSnapshot || {}, null, 2)" language="json" word-wrap /></n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<n-divider title-placement="left">状态时间线</n-divider>
|
||||
<n-timeline v-if="detail?.timeline?.length"><n-timeline-item v-for="item in detail.timeline" :key="`${item.title}-${item.time}`" :title="item.title" :time="item.time" /></n-timeline>
|
||||
<n-empty v-else description="暂无状态时间线" />
|
||||
<n-divider title-placement="left">资金流水</n-divider>
|
||||
<n-data-table :columns="ledgerColumns" :data="detail?.ledgers || []" :bordered="false" />
|
||||
<n-empty v-if="!detail?.ledgers?.length" description="暂无关联资金流水" />
|
||||
<n-divider title-placement="left">审批记录</n-divider>
|
||||
<n-data-table :columns="approvalColumns" :data="detail?.approvalActions || []" :bordered="false" />
|
||||
<n-empty v-if="!detail?.approvalActions?.length" description="暂无关联审批记录" />
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -77,11 +88,14 @@
|
||||
import { computed, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { NButton, NImage, NTag, useDialog, useMessage } from 'naive-ui';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { BasicTable, TableAction } from '@/components/Table';import { BasicForm,useForm } from '@/components/Form';import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { TradeOrderForcePay,TradeOrderFreeze,TradeOrderList,TradeOrderMatch,TradeOrderMatchProducts,TradeOrderUpdateAmount,TradeOrderView } from '@/api/trade';import { columns,schemas,loadOptions } from './model';
|
||||
const message=useMessage();const dialog=useDialog();const router=useRouter();const actionRef=ref(),searchFormRef=ref<any>({}),showDetail=ref(false),showMatch=ref(false),showAmount=ref(false),detail=ref<any>(),current=ref<any>(),autoRefresh=ref(false),refreshCountdown=ref(10);let timer:any;
|
||||
const message=useMessage();const dialog=useDialog();const router=useRouter();const dict=useDictStore();const actionRef=ref(),searchFormRef=ref<any>({}),showDetail=ref(false),showMatch=ref(false),showAmount=ref(false),detail=ref<any>(),current=ref<any>(),autoRefresh=ref(false),refreshCountdown=ref(10);let timer:any;
|
||||
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3 xl:5'},labelWidth:80,schemas});const loadDataTable=(res)=>TradeOrderList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();const yuan=(v)=>`¥${((v||0)/100).toFixed(2)}`;
|
||||
const yuanPlain=(v)=>((v||0)/100).toFixed(2);
|
||||
const ledgerColumns=[{title:'流水号',key:'ledgerNo',width:180},{title:'业务',key:'bizType',width:120},{title:'方向',key:'direction',width:80,render:(row)=>row.direction===1?'收入':'支出'},{title:'金额',key:'amount',width:110,render:(row)=>yuan(row.amount)},{title:'变动前',key:'beforeAmount',width:110,render:(row)=>yuan(row.beforeAmount)},{title:'变动后',key:'afterAmount',width:110,render:(row)=>yuan(row.afterAmount)},{title:'说明',key:'remark',minWidth:180},{title:'发生时间',key:'occurredAt',width:180}];
|
||||
const approvalColumns=[{title:'节点',key:'stepNo',width:70},{title:'操作人ID',key:'actorId',width:100},{title:'动作',key:'action',width:90},{title:'意见',key:'comment',minWidth:220},{title:'操作时间',key:'createdAt',width:180}];
|
||||
const matchLoading=ref(false),productLoading=ref(false),matchSubmitting=ref(false),products=ref<any[]>([]),selectedProduct=ref<any>();
|
||||
const matchMember=reactive({id:0,username:'',availableAmount:0,frozenAmount:0});
|
||||
const matchForm=reactive({id:0,productId:0,quantity:1});
|
||||
|
||||
@@ -9,6 +9,7 @@ export const schemas = ref<FormSchema[]>([
|
||||
{ field: 'orderNo', component: 'NInput', label: '订单号', componentProps: { placeholder: '请输入订单号', clearable: true } },
|
||||
{ field: 'username', component: 'NInput', label: '用户名', componentProps: { clearable: true } },
|
||||
{ field: 'mobile', component: 'NInput', label: '手机号', componentProps: { placeholder: '精确查询', clearable: true } },
|
||||
{ field: 'autoDispatch', component: 'NSelect', label: '派单模式', componentProps: { options: computed(() => dict.getOptionUnRef('tradeAutoDispatch')), clearable: true } },
|
||||
{ field: 'orderStatus', component: 'NSelect', label: '订单状态', componentProps: { options: computed(() => dict.getOptionUnRef('tradeOrderStatus')), clearable: true } },
|
||||
{ field: 'createdAt', component: 'NDatePicker', label: '下单时间', componentProps: { type: 'datetimerange', clearable: true, shortcuts: defRangeShortcuts() } },
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user