更新小程序和管理后台配置,将 API 地址切换为生产环境。新增撤回打款功能,允许用户在特定状态下撤回打款请求,并优化相关页面交互。更新文档以反映新的流程图,确保用户体验一致性。

This commit is contained in:
Alex-larget
2026-03-20 15:40:55 +08:00
parent d34d209b37
commit 0eee5a5fb7
10 changed files with 218 additions and 86 deletions

View File

@@ -13,8 +13,8 @@ const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
App({ App({
globalData: { globalData: {
// API 基础地址:开发时修改下面一行切换环境 // API 基础地址:开发时修改下面一行切换环境
// baseUrl: "https://soulapi.quwanzhi.com", baseUrl: "https://soulapi.quwanzhi.com",
baseUrl: 'http://localhost:8080', // 开发 // baseUrl: 'http://localhost:8080', // 开发
// baseUrl: 'https://souldev.quwanzhi.com', // 测试 // baseUrl: 'https://souldev.quwanzhi.com', // 测试
// 小程序配置 - 真实AppID // 小程序配置 - 真实AppID
appId: DEFAULT_APP_ID, appId: DEFAULT_APP_ID,

View File

@@ -4,6 +4,48 @@
--- ---
## 〇、总览(全流程)
```mermaid
flowchart TD
Start([触发]) --> Trigger{触发来源}
Trigger -->|onLaunch/onShow/登录| Main
Trigger -->|登录返回 isNewUser| LoginBranch
Trigger -->|VIP支付成功| VipPay
LoginBranch[登录成功] --> NewUser{isNewUser?}
NewUser -->|否| Main
NewUser -->|是| NeedAN1{头像昵称未改?}
NeedAN1 -->|是| ForceA[redirectTo avatar-nickname<br/>无弹窗]
NeedAN1 -->|否| Main
VipPay[VIP支付成功] --> Sync[同步权益] --> ModalV[弹窗:请填写好资料] --> RedirV[redirectTo profile-edit?from=vip]
Main[checkVipContactRequiredAndGuide] --> Skip{在 profile-edit<br/>或 avatar-nickname?}
Skip -->|是| End1([结束])
Skip -->|否| Fetch[请求 VIP + profile]
Fetch --> IsVip{VIP?}
IsVip -->|否| AvatarFlow[checkAvatarNicknameAndGuide]
AvatarFlow --> A1{头像昵称已完善?}
A1 -->|是| End2([结束])
A1 -->|否| A2{今日已提示?}
A2 -->|是| End2
A2 -->|否| MA[弹窗:请设置头像和昵称] --> NavA[navigateTo avatar-nickname]
IsVip -->|是| B1{头像昵称未改?}
B1 -->|是| M1[弹窗:请完善资料] --> R1[redirectTo profile-edit]
B1 -->|否| B2{有手机号?}
B2 -->|否| M2[弹窗VIP需完善手机号] --> R2[redirectTo profile-edit]
B2 -->|是| B3{有微信号?}
B3 -->|否| M3[弹窗:请完善微信号] --> N1[navigateTo profile-edit]
B3 -->|是| End3([结束])
```
**触发时机**onLaunch 1.5s | onShow 0.5s(节流 5min| 登录成功 1.2s | VIP 支付成功 即时
---
## 一、整体流程(冷启动 / onShow ## 一、整体流程(冷启动 / onShow
```mermaid ```mermaid

View File

@@ -1,8 +1,8 @@
# 对接后端 base URL不改 API 路径,仅改此处即可切换 Next → Gin # 对接后端 base URL不改 API 路径,仅改此处即可切换 Next → Gin
# 宝塔部署:若 API 站点开启了强制 HTTPS这里必须用 https否则预检请求会被重定向导致 CORS 报错 # 宝塔部署:若 API 站点开启了强制 HTTPS这里必须用 https否则预检请求会被重定向导致 CORS 报错
# VITE_API_BASE_URL=http://localhost:3006 # VITE_API_BASE_URL=http://localhost:3006
VITE_API_BASE_URL=http://localhost:8080 # VITE_API_BASE_URL=http://localhost:8080
# VITE_API_BASE_URL=https://soulapi.quwanzhi.com VITE_API_BASE_URL=https://soulapi.quwanzhi.com
# VITE_API_BASE_URL=https://souldev.quwanzhi.com # VITE_API_BASE_URL=https://souldev.quwanzhi.com

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Soul创业派对</title> <title>管理后台 - Soul创业派对</title>
<script type="module" crossorigin src="/assets/index-DCoaVA6V.js"></script> <script type="module" crossorigin src="/assets/index-ByhYXHAh.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DGXqHqcA.css"> <link rel="stylesheet" crossorigin href="/assets/index-DGXqHqcA.css">
</head> </head>
<body> <body>

View File

@@ -32,7 +32,7 @@ import {
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { get, put } from '@/api/client' import { get, put, post } from '@/api/client'
interface TodayClicksByPageItem { interface TodayClicksByPageItem {
page: string page: string
@@ -387,6 +387,25 @@ export function DistributionPage() {
setRejectReason('') setRejectReason('')
} }
async function handleCancelWithdrawal(id: string) {
if (!confirm('确认撤回该笔打款?资金将退回商户运营账户。')) return
try {
const res = await post<{ success?: boolean; error?: string; message?: string }>(
'/api/admin/withdrawals/cancel',
{ id },
)
if (!res?.success) {
toast.error(res?.error || res?.message || '撤回失败')
return
}
toast.success('已撤回打款')
await refreshCurrentTab()
} catch (e) {
console.error(e)
toast.error('撤回失败')
}
}
async function submitRejectWithdrawal() { async function submitRejectWithdrawal() {
const id = rejectWithdrawalId const id = rejectWithdrawalId
if (!id) return if (!id) return
@@ -1354,6 +1373,19 @@ export function DistributionPage() {
</Button> </Button>
</div> </div>
)} )}
{(withdrawal.status === 'pending_confirm' || withdrawal.status === 'processing') && (
<div className="flex gap-2 justify-end">
<Button
size="sm"
variant="outline"
onClick={() => handleCancelWithdrawal(withdrawal.id)}
className="border-amber-500/50 text-amber-400 hover:bg-amber-500/20"
>
<Undo2 className="w-4 h-4 mr-1" />
</Button>
</div>
)}
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -351,6 +351,45 @@ func AdminWithdrawalsAction(c *gin.Context) {
} }
} }
// AdminWithdrawalsCancel POST /api/admin/withdrawals/cancel 撤回打款(仅 processing/pending_confirm 可撤回)
func AdminWithdrawalsCancel(c *gin.Context) {
var body struct {
ID string `json:"id"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
return
}
db := database.DB()
var w model.Withdrawal
if err := db.Where("id = ?", body.ID).First(&w).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "提现记录不存在"})
return
}
st := ""
if w.Status != nil {
st = *w.Status
}
if st != "processing" && st != "pending_confirm" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "当前状态不允许撤回,仅待确认收款时可撤回"})
return
}
outBillNo := body.ID
if w.DetailNo != nil && *w.DetailNo != "" {
outBillNo = *w.DetailNo
}
if err := wechat.CancelTransferByOutBill(outBillNo); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "撤回失败: " + err.Error()})
return
}
now := time.Now()
reason := "商户已撤回"
_ = db.Model(&w).Updates(map[string]interface{}{
"status": "failed", "fail_reason": reason, "error_message": reason, "processed_at": now,
}).Error
c.JSON(http.StatusOK, gin.H{"success": true, "message": "已撤回打款"})
}
// AdminWithdrawalsSync POST /api/admin/withdrawals/sync 主动向微信查询转账结果并更新状态(无回调时的备选) // AdminWithdrawalsSync POST /api/admin/withdrawals/sync 主动向微信查询转账结果并更新状态(无回调时的备选)
// body: { "id": "提现记录id" } 同步单条;不传 id 或 id 为空则同步所有 processing/pending_confirm // body: { "id": "提现记录id" } 同步单条;不传 id 或 id 为空则同步所有 processing/pending_confirm
func AdminWithdrawalsSync(c *gin.Context) { func AdminWithdrawalsSync(c *gin.Context) {

View File

@@ -118,7 +118,14 @@ func WithdrawPost(c *gin.Context) {
if err := db.Where("config_key = ?", "referral_config").First(&refCfg).Error; err == nil { if err := db.Where("config_key = ?", "referral_config").First(&refCfg).Error; err == nil {
var config map[string]interface{} var config map[string]interface{}
if _ = json.Unmarshal(refCfg.ConfigValue, &config); config != nil { if _ = json.Unmarshal(refCfg.ConfigValue, &config); config != nil {
if enabled, ok := config["enableAutoWithdraw"].(bool); ok && enabled { enabled := false
switch v := config["enableAutoWithdraw"].(type) {
case bool:
enabled = v
case float64:
enabled = v != 0
}
if enabled {
go func(id string) { go func(id string) {
if _, e := doApproveWithdrawal(db, id); e != nil { if _, e := doApproveWithdrawal(db, id); e != nil {
fmt.Printf("[WithdrawPost] 自动审批失败 id=%s: %v\n", id, e) fmt.Printf("[WithdrawPost] 自动审批失败 id=%s: %v\n", id, e)

View File

@@ -77,6 +77,7 @@ func Setup(cfg *config.Config) *gin.Engine {
admin.DELETE("/referral", handler.AdminReferral) admin.DELETE("/referral", handler.AdminReferral)
admin.GET("/withdrawals", handler.AdminWithdrawalsList) admin.GET("/withdrawals", handler.AdminWithdrawalsList)
admin.PUT("/withdrawals", handler.AdminWithdrawalsAction) admin.PUT("/withdrawals", handler.AdminWithdrawalsAction)
admin.POST("/withdrawals/cancel", handler.AdminWithdrawalsCancel)
admin.POST("/withdrawals/sync", handler.AdminWithdrawalsSync) admin.POST("/withdrawals/sync", handler.AdminWithdrawalsSync)
admin.GET("/withdrawals/auto-approve", handler.AdminWithdrawalsAutoApproveGet) admin.GET("/withdrawals/auto-approve", handler.AdminWithdrawalsAutoApproveGet)
admin.PUT("/withdrawals/auto-approve", handler.AdminWithdrawalsAutoApprovePut) admin.PUT("/withdrawals/auto-approve", handler.AdminWithdrawalsAutoApprovePut)

View File

@@ -213,6 +213,17 @@ func InitiateTransferByFundApp(params FundAppTransferParams) (*FundAppTransferRe
return result, nil return result, nil
} }
// CancelTransferByOutBill 撤销转账(用户确认收款前,商户可主动撤回)
// 接口POST /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel
func CancelTransferByOutBill(outBillNo string) error {
if paymentApp == nil || paymentApp.FundApp == nil {
return fmt.Errorf("支付/转账未初始化")
}
ctx := context.Background()
_, err := paymentApp.FundApp.Cancel(ctx, outBillNo)
return err
}
// QueryTransferByOutBill 按商户单号查询单笔转账结果FundApp 接口,用于 sync // QueryTransferByOutBill 按商户单号查询单笔转账结果FundApp 接口,用于 sync
func QueryTransferByOutBill(outBillNo string) (state, transferBillNo, failReason string, err error) { func QueryTransferByOutBill(outBillNo string) (state, transferBillNo, failReason string, err error) {
if paymentApp == nil || paymentApp.FundApp == nil { if paymentApp == nil || paymentApp.FundApp == nil {