更新小程序配置,切换API基础地址至本地开发环境。优化用户提交联系方式的逻辑,增加2分钟内限频提示,确保用户体验流畅。调整后端存客宝接口,支持记录用户提交信息并处理频率限制。
This commit is contained in:
@@ -8,9 +8,9 @@ const { parseScene } = require('./utils/scene.js')
|
||||
App({
|
||||
globalData: {
|
||||
// API基础地址 - 连接真实后端
|
||||
baseUrl: 'https://soulapi.quwanzhi.com',
|
||||
// baseUrl: 'https://soulapi.quwanzhi.com',
|
||||
// baseUrl: 'https://souldev.quwanzhi.com',
|
||||
// baseUrl: 'http://localhost:8080',
|
||||
baseUrl: 'http://localhost:8080',
|
||||
|
||||
|
||||
// 小程序配置 - 真实AppID
|
||||
|
||||
@@ -320,7 +320,12 @@ Page({
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
const leadKey = 'karuo_lead_' + userId
|
||||
// 2 分钟内只能点一次(与后端限频一致)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
let phone = (app.globalData.userInfo.phone || '').trim()
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
|
||||
if (!phone && !wechatId) {
|
||||
@@ -333,11 +338,6 @@ Page({
|
||||
} catch (e) {}
|
||||
}
|
||||
if (phone || wechatId) {
|
||||
const hasLead = wx.getStorageSync(leadKey)
|
||||
if (hasLead) {
|
||||
wx.showToast({ title: '已提交联系方式,卡若会尽快联系你', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
@@ -352,7 +352,7 @@ Page({
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res && res.success) {
|
||||
wx.setStorageSync(leadKey, true)
|
||||
wx.setStorageSync('lead_last_submit_ts', Date.now())
|
||||
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
@@ -384,9 +384,14 @@ Page({
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 2 分钟内只能点一次(与后端限频一致)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const app = getApp()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const leadKey = userId ? ('karuo_lead_' + userId) : ''
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
@@ -401,7 +406,7 @@ Page({
|
||||
wx.hideLoading()
|
||||
this.setData({ showLeadModal: false, leadPhone: '' })
|
||||
if (res && res.success) {
|
||||
if (leadKey) wx.setStorageSync(leadKey, true)
|
||||
wx.setStorageSync('lead_last_submit_ts', Date.now())
|
||||
wx.showToast({ title: res.message || '提交成功,卡若会尽快联系您', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
|
||||
@@ -551,9 +551,10 @@ Page({
|
||||
})
|
||||
return
|
||||
}
|
||||
const leadKey = `mention_lead_${myUserId}_${targetUserId}`
|
||||
if (wx.getStorageSync(leadKey)) {
|
||||
wx.showToast({ title: '已提交过,对方会尽快联系您', icon: 'none' })
|
||||
// 2 分钟内只能点一次(与后端限频一致,与首页链接卡若共用)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
@@ -573,7 +574,7 @@ Page({
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res && res.success) {
|
||||
wx.setStorageSync(leadKey, true)
|
||||
wx.setStorageSync('lead_last_submit_ts', Date.now())
|
||||
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
|
||||
@@ -42,3 +42,6 @@ WECHAT_SERIAL_NO=4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5
|
||||
|
||||
# 跨域 CORS:允许的源,逗号分隔。未设置时使用默认值(含 localhost、soul.quwanzhi.com)
|
||||
CORS_ORIGINS=http://localhost:5175,http://localhost:5174,http://127.0.0.1:5174,https://soul.quwanzhi.com,http://soul.quwanzhi.com,https://souladmin.quwanzhi.com,http://souladmin.quwanzhi.com
|
||||
|
||||
# 存客宝-链接卡若:请求到存客宝添加好友使用的 apiKey(与 join/match 不同)
|
||||
CKB_LEAD_API_KEY=2y4v5-rjhfc-sg5wy-zklkv-bg0tl
|
||||
|
||||
@@ -44,3 +44,6 @@ WECHAT_SERIAL_NO=4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5
|
||||
|
||||
# 跨域 CORS:允许的源,逗号分隔。未设置时使用默认值(含 localhost、soul.quwanzhi.com)
|
||||
CORS_ORIGINS=http://localhost:5175,http://localhost:5174,http://127.0.0.1:5174,https://soul.quwanzhi.com,http://soul.quwanzhi.com,https://souladmin.quwanzhi.com,http://souladmin.quwanzhi.com
|
||||
|
||||
# 存客宝-链接卡若:请求到存客宝添加好友使用的 apiKey(与 join/match 不同)
|
||||
CKB_LEAD_API_KEY=2y4v5-rjhfc-sg5wy-zklkv-bg0tl
|
||||
|
||||
@@ -43,6 +43,9 @@ type Config struct {
|
||||
|
||||
// 订单对账定时任务间隔(分钟),0 表示不启动内置定时任务
|
||||
SyncOrdersIntervalMinutes int
|
||||
|
||||
// 存客宝-链接卡若:可选,用于「添加好友」的 apiKey(与 GET scenarios 示例一致时可填 CKB_LEAD_API_KEY)
|
||||
CkbLeadAPIKey string
|
||||
}
|
||||
|
||||
// BaseURLJoin 将路径拼接到 BaseURL,path 应以 / 开头
|
||||
@@ -253,5 +256,6 @@ func Load() (*Config, error) {
|
||||
AdminPassword: adminPassword,
|
||||
AdminSessionSecret: adminSessionSecret,
|
||||
SyncOrdersIntervalMinutes: syncOrdersInterval,
|
||||
CkbLeadAPIKey: strings.TrimSpace(os.Getenv("CKB_LEAD_API_KEY")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ func Init(dsn string) error {
|
||||
if err := db.AutoMigrate(&model.AdminUser{}); err != nil {
|
||||
log.Printf("database: admin_users migrate warning: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.CkbSubmitRecord{}); err != nil {
|
||||
log.Printf("database: ckb_submit_records migrate warning: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.CkbLeadRecord{}); err != nil {
|
||||
log.Printf("database: ckb_lead_records migrate warning: %v", err)
|
||||
}
|
||||
log.Println("database: connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -15,16 +16,31 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"soul-api/internal/config"
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
)
|
||||
|
||||
// 存客宝 API Key 约定(详见 开发文档/8、部署/存客宝API-Key约定.md):
|
||||
// - 链接卡若(添加卡若好友):使用 CKB_LEAD_API_KEY(.env 配置),未配则用下方 ckbAPIKey
|
||||
// - 其他场景(join/match 等):使用 ckbAPIKey
|
||||
const ckbAPIKey = "fyngh-ecy9h-qkdae-epwd5-rz6kd"
|
||||
const ckbAPIURL = "https://ckbapi.quwanzhi.com/v1/api/scenarios"
|
||||
|
||||
var ckbSourceMap = map[string]string{"team": "团队招募", "investor": "资源对接", "mentor": "导师顾问", "partner": "创业合伙"}
|
||||
var ckbTagsMap = map[string]string{"team": "切片团队,团队招募", "investor": "资源对接,资源群", "mentor": "导师顾问,咨询服务", "partner": "创业合伙,创业伙伴"}
|
||||
|
||||
// ckbSubmitSave 加好友/留资类接口统一落库:记录 action、userId、昵称、用户提交的传参,写入 ckb_submit_records
|
||||
func ckbSubmitSave(action, userID, nickname string, params interface{}) {
|
||||
paramsJSON, _ := json.Marshal(params)
|
||||
_ = database.DB().Create(&model.CkbSubmitRecord{
|
||||
Action: action,
|
||||
UserID: userID,
|
||||
Nickname: nickname,
|
||||
Params: string(paramsJSON),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ckbSign 与 next-project app/api/ckb/join 一致:排除 sign/apiKey/portrait,空值跳过,按键升序拼接值,MD5(拼接串) 再 MD5(结果+apiKey)
|
||||
func ckbSign(params map[string]interface{}, apiKey string) string {
|
||||
keys := make([]string, 0, len(params))
|
||||
@@ -85,6 +101,20 @@ func CKBJoin(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的加入类型"})
|
||||
return
|
||||
}
|
||||
nickname := strings.TrimSpace(body.Name)
|
||||
if nickname == "" && body.UserID != "" {
|
||||
var u model.User
|
||||
if database.DB().Select("nickname").Where("id = ?", body.UserID).First(&u).Error == nil && u.Nickname != nil && *u.Nickname != "" {
|
||||
nickname = *u.Nickname
|
||||
}
|
||||
}
|
||||
if nickname == "" {
|
||||
nickname = "-"
|
||||
}
|
||||
ckbSubmitSave("join", body.UserID, nickname, map[string]interface{}{
|
||||
"type": body.Type, "phone": body.Phone, "wechat": body.Wechat, "name": body.Name,
|
||||
"userId": body.UserID, "remark": body.Remark, "canHelp": body.CanHelp, "needHelp": body.NeedHelp,
|
||||
})
|
||||
ts := time.Now().Unix()
|
||||
params := map[string]interface{}{
|
||||
"timestamp": ts,
|
||||
@@ -195,6 +225,14 @@ func CKBMatch(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请提供手机号或微信号"})
|
||||
return
|
||||
}
|
||||
nickname := strings.TrimSpace(body.Nickname)
|
||||
if nickname == "" {
|
||||
nickname = "-"
|
||||
}
|
||||
ckbSubmitSave("match", body.UserID, nickname, map[string]interface{}{
|
||||
"matchType": body.MatchType, "phone": body.Phone, "wechat": body.Wechat,
|
||||
"userId": body.UserID, "nickname": body.Nickname, "matchedUser": body.MatchedUser,
|
||||
})
|
||||
ts := time.Now().Unix()
|
||||
label := ckbSourceMap[body.MatchType]
|
||||
if label == "" {
|
||||
@@ -279,12 +317,58 @@ func CKBLead(c *gin.Context) {
|
||||
if name == "" {
|
||||
name = "小程序用户"
|
||||
}
|
||||
db := database.DB()
|
||||
var cond []string
|
||||
var args []interface{}
|
||||
if body.UserID != "" {
|
||||
cond = append(cond, "user_id = ?")
|
||||
args = append(args, body.UserID)
|
||||
}
|
||||
if phone != "" {
|
||||
cond = append(cond, "phone = ?")
|
||||
args = append(args, phone)
|
||||
}
|
||||
if wechatId != "" {
|
||||
cond = append(cond, "wechat_id = ?")
|
||||
args = append(args, wechatId)
|
||||
}
|
||||
// 2 分钟内同一用户/手机/微信只能提交一次(与前端限频一致)
|
||||
if len(cond) > 0 {
|
||||
cutoff := time.Now().Add(-2 * time.Minute)
|
||||
var recentCount int64
|
||||
if db.Model(&model.CkbLeadRecord{}).Where(strings.Join(cond, " OR "), args...).Where("created_at > ?", cutoff).Count(&recentCount) == nil && recentCount > 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "您操作太频繁,请2分钟后再试"})
|
||||
return
|
||||
}
|
||||
}
|
||||
// 是否曾留资过(仅用于成功后的提示文案)
|
||||
repeatedSubmit := false
|
||||
if len(cond) > 0 {
|
||||
var existCount int64
|
||||
repeatedSubmit = db.Model(&model.CkbLeadRecord{}).Where(strings.Join(cond, " OR "), args...).Count(&existCount) == nil && existCount > 0
|
||||
}
|
||||
|
||||
paramsJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"userId": body.UserID, "phone": phone, "wechatId": wechatId, "name": body.Name,
|
||||
})
|
||||
_ = db.Create(&model.CkbLeadRecord{
|
||||
UserID: body.UserID,
|
||||
Nickname: name,
|
||||
Phone: phone,
|
||||
WechatID: wechatId,
|
||||
Name: strings.TrimSpace(body.Name),
|
||||
Params: string(paramsJSON),
|
||||
}).Error
|
||||
ts := time.Now().Unix()
|
||||
// 链接卡若:GET + query(便于浏览器测试),传参:name, phone, wechatId, apiKey, timestamp, sign
|
||||
leadKey := ckbAPIKey
|
||||
if cfg := config.Get(); cfg != nil && cfg.CkbLeadAPIKey != "" {
|
||||
leadKey = cfg.CkbLeadAPIKey
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"timestamp": ts,
|
||||
"source": "小程序-链接卡若",
|
||||
"remark": "首页点击「链接卡若」留资",
|
||||
"name": name,
|
||||
"timestamp": ts,
|
||||
"apiKey": leadKey,
|
||||
}
|
||||
if phone != "" {
|
||||
params["phone"] = phone
|
||||
@@ -292,19 +376,30 @@ func CKBLead(c *gin.Context) {
|
||||
if wechatId != "" {
|
||||
params["wechatId"] = wechatId
|
||||
}
|
||||
params["apiKey"] = ckbAPIKey
|
||||
params["sign"] = ckbSign(params, ckbAPIKey)
|
||||
raw, _ := json.Marshal(params)
|
||||
resp, err := http.Post(ckbAPIURL, "application/json", bytes.NewReader(raw))
|
||||
params["sign"] = ckbSign(params, leadKey)
|
||||
q := url.Values{}
|
||||
q.Set("name", name)
|
||||
q.Set("timestamp", strconv.FormatInt(ts, 10))
|
||||
q.Set("apiKey", leadKey)
|
||||
if phone != "" {
|
||||
q.Set("phone", phone)
|
||||
}
|
||||
if wechatId != "" {
|
||||
q.Set("wechatId", wechatId)
|
||||
}
|
||||
q.Set("sign", params["sign"].(string))
|
||||
reqURL := ckbAPIURL + "?" + q.Encode()
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
fmt.Printf("[CKBLead] 请求存客宝失败: %v\n", err)
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "网络异常,请稍后重试"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(b, &result)
|
||||
@@ -313,12 +408,23 @@ func CKBLead(c *gin.Context) {
|
||||
if result.Message == "已存在" {
|
||||
msg = "您已留资,我们会尽快联系您"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": result.Data})
|
||||
if repeatedSubmit {
|
||||
msg = "您已留资过,我们已再次通知卡若,请耐心等待添加"
|
||||
}
|
||||
data := gin.H{}
|
||||
if result.Data != nil {
|
||||
if m, ok := result.Data.(map[string]interface{}); ok {
|
||||
data = m
|
||||
}
|
||||
}
|
||||
data["repeatedSubmit"] = repeatedSubmit
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg, "data": data})
|
||||
return
|
||||
}
|
||||
errMsg := result.Message
|
||||
if errMsg == "" {
|
||||
errMsg = "提交失败,请稍后重试"
|
||||
}
|
||||
fmt.Printf("[CKBLead] 存客宝返回异常 code=%d message=%s raw=%s\n", result.Code, result.Message, string(b))
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": errMsg})
|
||||
}
|
||||
|
||||
17
soul-api/internal/model/ckb_lead.go
Normal file
17
soul-api/internal/model/ckb_lead.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// CkbLeadRecord 链接卡若留资记录(独立表,便于后续链接其他用户等扩展)
|
||||
type CkbLeadRecord struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserID string `gorm:"column:user_id;size:50;index" json:"userId"`
|
||||
Nickname string `gorm:"column:nickname;size:100" json:"nickname"`
|
||||
Phone string `gorm:"column:phone;size:20" json:"phone"`
|
||||
WechatID string `gorm:"column:wechat_id;size:100" json:"wechatId"`
|
||||
Name string `gorm:"column:name;size:100" json:"name"` // 用户填的姓名/昵称
|
||||
Params string `gorm:"column:params;type:json" json:"params"` // 完整传参 JSON
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
}
|
||||
|
||||
func (CkbLeadRecord) TableName() string { return "ckb_lead_records" }
|
||||
15
soul-api/internal/model/ckb_submit.go
Normal file
15
soul-api/internal/model/ckb_submit.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// CkbSubmitRecord 加好友/留资类接口提交记录(存客宝 lead/join/match)
|
||||
type CkbSubmitRecord struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Action string `gorm:"column:action;size:20;not null" json:"action"` // lead | join | match
|
||||
UserID string `gorm:"column:user_id;size:50;index" json:"userId"` // 用户 id
|
||||
Nickname string `gorm:"column:nickname;size:100" json:"nickname"` // 昵称
|
||||
Params string `gorm:"column:params;type:json;not null" json:"params"` // 用户提交的传参 JSON
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
}
|
||||
|
||||
func (CkbSubmitRecord) TableName() string { return "ckb_submit_records" }
|
||||
28
开发文档/8、部署/存客宝API-Key约定.md
Normal file
28
开发文档/8、部署/存客宝API-Key约定.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 存客宝 API Key 约定
|
||||
|
||||
## 约定说明
|
||||
|
||||
存客宝(ckbapi.quwanzhi.com)不同业务使用**不同的 apiKey**,对接时需按场景选用,避免混用。
|
||||
|
||||
| 场景 | 用途 | Key 来源 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| **链接卡若** | 首页「链接卡若」留资,添加卡若为好友 | 环境变量 `CKB_LEAD_API_KEY` | 需在 .env 中配置;未配置时回退为下方「其他场景」的 key;请求方式为 **POST** + JSON(name, phone, wechatId, apiKey, timestamp, sign) |
|
||||
| **其他** | join(团队/资源/导师/合伙)、match(找伙伴匹配)等 | 代码常量 `ckbAPIKey` | 当前为 `fyngh-ecy9h-qkdae-epwd5-rz6kd` |
|
||||
|
||||
## 配置示例
|
||||
|
||||
- **链接卡若**(添加好友需用专用 key,示例):
|
||||
```env
|
||||
CKB_LEAD_API_KEY=2y4v5-rjhfc-sg5wy-zklkv-bg0tl
|
||||
```
|
||||
- 后续若有其他「添加某某为好友」类场景,由存客宝提供对应 key,再在配置或代码中单独挂接,**不要与链接卡若的 key 混用**。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- soul-api:`internal/handler/ckb.go`
|
||||
- 链接卡若:`CKBLead` 中读取 `config.Get().CkbLeadAPIKey`,有则用,无则用 `ckbAPIKey`
|
||||
- join/match:统一使用 `ckbAPIKey`
|
||||
|
||||
---
|
||||
|
||||
记录时间:2025-03;原因:链接卡若需专用 key 才能正常添加好友,其他场景用另一 key。
|
||||
Reference in New Issue
Block a user