diff --git a/app/main/api/desc/admin/order.api b/app/main/api/desc/admin/order.api index 3a7b470..ffe2265 100644 --- a/app/main/api/desc/admin/order.api +++ b/app/main/api/desc/admin/order.api @@ -77,6 +77,9 @@ type ( RefundTimeStart string `form:"refund_time_start,optional"` // 退款时间开始 RefundTimeEnd string `form:"refund_time_end,optional"` // 退款时间结束 SalesCost float64 `form:"sales_cost,optional"` // 成本价 + QueryName string `form:"query_name,optional"` // 被查询人姓名(通过 query_user_record 表追溯订单) + QueryIdCard string `form:"query_id_card,optional"` // 被查询人身份证(通过 query_user_record 表追溯订单) + QueryMobile string `form:"query_mobile,optional"` // 被查询人手机号(通过 query_user_record 表追溯订单) } // 列表响应 AdminGetOrderListResp { diff --git a/app/main/api/etc/main.dev.yaml b/app/main/api/etc/main.dev.yaml index 27b4056..d53bb17 100644 --- a/app/main/api/etc/main.dev.yaml +++ b/app/main/api/etc/main.dev.yaml @@ -1,13 +1,13 @@ Name: main Host: 0.0.0.0 Port: 8888 -DataSource: "znc:5vg67b3UNHu823@tcp(127.0.0.1:21001)/znc?charset=utf8mb4&parseTime=True&loc=Local" -# ✅ 正确:使用容器名称 znc_mysql +DataSource: "znc:5vg67b3UNHu823@tcp(localhost:21101)/znc?charset=utf8mb4&parseTime=True&loc=Local" +# ✅ 本地开发:使用 localhost 和 Docker 映射端口 CacheRedis: - - Host: "127.0.0.1:21002" + - Host: "localhost:21102" Pass: "3m3WsgyCKWqz" # Redis 密码,如果未设置则留空 Type: "node" # 单节点模式 - Timeout: 10000 # 连接超时时间(毫秒),默认2000 + Timeout: 30000 # 连接超时时间(毫秒),增加到30秒 JwtAuth: AccessSecret: "WUvoIwL-FK0qnlxhvxR9tV6SjfOpeJMpKmY2QvT99lA" AccessExpire: 2592000 diff --git a/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go b/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go index 3c831c3..0a97e24 100644 --- a/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go +++ b/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go @@ -2,6 +2,8 @@ package admin_order import ( "context" + "encoding/hex" + "strings" "sync" "tydata-server/app/main/api/internal/svc" @@ -9,6 +11,7 @@ import ( "tydata-server/app/main/model" "tydata-server/common/globalkey" "tydata-server/common/xerr" + "tydata-server/pkg/lzkit/crypto" "github.com/Masterminds/squirrel" "github.com/pkg/errors" @@ -77,6 +80,19 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR builder = builder.Where("refund_time <= ?", req.RefundTimeEnd) } + // 按被查询人(query_user_record 表)过滤:姓名、身份证、手机号(库中为密文,需解密后匹配) + if req.QueryName != "" || req.QueryIdCard != "" || req.QueryMobile != "" { + orderIds, filterErr := l.filterOrderIdsByQueryUserRecord(req.QueryName, req.QueryIdCard, req.QueryMobile) + if filterErr != nil { + return nil, filterErr + } + if len(orderIds) == 0 { + builder = builder.Where("1 = 0") // 无匹配时返回空 + } else { + builder = builder.Where(squirrel.Eq{"id": orderIds}) + } + } + // 并发获取总数和列表 var total int64 var orders []*model.Order @@ -294,3 +310,71 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR return resp, nil } + +// filterOrderIdsByQueryUserRecord 根据姓名、身份证、手机号(明文)从 query_user_record 解密后匹配,返回符合条件的 order_id 列表 +func (l *AdminGetOrderListLogic) filterOrderIdsByQueryUserRecord(queryName, queryIdCard, queryMobile string) ([]int64, error) { + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, keyErr := hex.DecodeString(secretKey) + if keyErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "Encrypt.SecretKey 解析失败: %v", keyErr) + } + + qb := l.svcCtx.QueryUserRecordModel.SelectBuilder().Where("order_id > ?", 0) + recs, err := l.svcCtx.QueryUserRecordModel.FindAll(l.ctx, qb, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询 query_user_record 失败: %v", err) + } + + orderIds := make([]int64, 0, len(recs)) + for _, rec := range recs { + match := true + + if queryName != "" { + var decName string + if rec.Name != "" { + bs, e := crypto.AesEcbDecrypt(rec.Name, key) + if e != nil { + match = false + } else { + decName = string(bs) + } + } + if match && !strings.Contains(decName, queryName) { + match = false + } + } + + if match && queryIdCard != "" { + var decIdCard string + if rec.IdCard != "" { + var e error + decIdCard, e = crypto.DecryptIDCard(rec.IdCard, key) + if e != nil { + match = false + } + } + if match && decIdCard != queryIdCard { + match = false + } + } + + if match && queryMobile != "" { + var decMobile string + if rec.Mobile != "" { + var e error + decMobile, e = crypto.DecryptMobile(rec.Mobile, secretKey) + if e != nil { + match = false + } + } + if match && decMobile != queryMobile { + match = false + } + } + + if match { + orderIds = append(orderIds, rec.OrderId) + } + } + return orderIds, nil +} diff --git a/app/main/api/internal/logic/agent/agentrealnamelogic.go b/app/main/api/internal/logic/agent/agentrealnamelogic.go index 9cd83e0..88973b1 100644 --- a/app/main/api/internal/logic/agent/agentrealnamelogic.go +++ b/app/main/api/internal/logic/agent/agentrealnamelogic.go @@ -44,14 +44,8 @@ func (l *AgentRealNameLogic) AgentRealName(req *types.AgentRealNameReq) (resp *t if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "代理实名, 加密手机号失败: %v", err) } - // 开发环境固定验证码为138888 - env := os.Getenv("ENV") - if env == "development" { - if req.Code != "138888" { - return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "开发环境验证码应为138888") - } - logx.Infof("开发环境:验证码验证通过 %s", req.Mobile) - } else { + // 开发环境下跳过验证码验证 + if os.Getenv("ENV") != "development" { // 检查手机号是否在一分钟内已发送过验证码 redisKey := fmt.Sprintf("%s:%s", "realName", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) diff --git a/app/main/api/internal/logic/agent/applyforagentlogic.go b/app/main/api/internal/logic/agent/applyforagentlogic.go index c108d47..da88cb4 100644 --- a/app/main/api/internal/logic/agent/applyforagentlogic.go +++ b/app/main/api/internal/logic/agent/applyforagentlogic.go @@ -2,14 +2,14 @@ package agent import ( "context" - "tydata-server/app/main/model" - "tydata-server/common/ctxdata" - "tydata-server/common/xerr" - "tydata-server/pkg/lzkit/crypto" "database/sql" "fmt" "os" "time" + "tydata-server/app/main/model" + "tydata-server/common/ctxdata" + "tydata-server/common/xerr" + "tydata-server/pkg/lzkit/crypto" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/stores/redis" @@ -45,14 +45,8 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) } - // 开发环境固定验证码为138888 - env := os.Getenv("ENV") - if env == "development" { - if req.Code != "138888" { - return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "开发环境验证码应为138888") - } - logx.Infof("开发环境:验证码验证通过 %s", req.Mobile) - } else if req.Mobile != "18889793585" { + // 开发环境下跳过验证码验证,或者特定手机号跳过(保留原有逻辑) + if os.Getenv("ENV") != "development" && req.Mobile != "18889793585" { // 校验验证码 redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) diff --git a/app/main/api/internal/logic/pay/alipaycallbacklogic.go b/app/main/api/internal/logic/pay/alipaycallbacklogic.go index b277497..4ac598c 100644 --- a/app/main/api/internal/logic/pay/alipaycallbacklogic.go +++ b/app/main/api/internal/logic/pay/alipaycallbacklogic.go @@ -93,7 +93,6 @@ func (l *AlipayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, not logx.Errorf("支付宝支付回调,修改订单信息失败: %+v", updateErr) return nil } - if order.Status == "paid" { if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil { logx.Errorf("异步任务调度失败: %v", asyncErr) diff --git a/app/main/api/internal/logic/pay/paymentlogic.go b/app/main/api/internal/logic/pay/paymentlogic.go index 5cf34bd..dae487f 100644 --- a/app/main/api/internal/logic/pay/paymentlogic.go +++ b/app/main/api/internal/logic/pay/paymentlogic.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "time" + "tydata-server/app/main/api/internal/svc" "tydata-server/app/main/api/internal/types" "tydata-server/app/main/model" @@ -30,7 +31,7 @@ type PaymentTypeResp struct { amount float64 outTradeNo string description string - orderID int64 + orderID int64 // 仅 query 类型有值;agent_vip 为 0 } func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic { @@ -44,6 +45,7 @@ func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLo func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, err error) { var paymentTypeResp *PaymentTypeResp var prepayData interface{} + l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { switch req.PayType { case "agent_vip": @@ -59,6 +61,17 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, } } + // 开发环境测试支付模式:仅当 pay_method=test 时跳过实际支付,直接返回 test_payment_success + // 支付宝/微信在开发环境下仍走真实支付流程(跳转沙箱),支付成功后由回调更新订单 + // 注意:订单状态更新在事务外进行,避免在事务中查询不到订单的问题 + isDevTestPayment := os.Getenv("ENV") == "development" && req.PayMethod == "test" + if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID != 0 { + prepayData = "test_payment_success" + logx.Infof("开发环境测试支付模式:订单 %s (ID: %d) 将在事务提交后更新状态", paymentTypeResp.outTradeNo, paymentTypeResp.orderID) + return nil + } + + // 仅 wechat/alipay/appleiap 调起真实支付;test 仅在上面 isDevTestPayment 分支处理 var createOrderErr error if req.PayMethod == "wechat" { prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) @@ -66,6 +79,13 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) } else if req.PayMethod == "appleiap" { prepayData = l.svcCtx.ApplePayService.GetIappayAppID(paymentTypeResp.outTradeNo) + } else if req.PayMethod == "test" { + if os.Getenv("ENV") != "development" { + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "开发环境测试支付仅在开发环境可用") + } + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "开发环境测试支付仅支持 query 类型订单") + } else { + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "不支持的支付方式: %s", req.PayMethod) } if createOrderErr != nil { return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 创建支付订单失败: %+v", createOrderErr) @@ -75,37 +95,43 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, if err != nil { return nil, err } - - // 开发环境测试支付模式:事务提交后处理订单状态更新和后续流程 - isDevTestPayment := os.Getenv("ENV") == "development" + // 开发环境测试支付模式:事务提交后处理订单状态更新和后续流程(仅 pay_method=test 且 query 类型 orderID>0) + isDevTestPayment := os.Getenv("ENV") == "development" && req.PayMethod == "test" if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID != 0 { - // 使用 goroutine 异步处理,确保事务已完全提交 go func() { - // 短暂延迟,确保事务已完全提交到数据库 time.Sleep(200 * time.Millisecond) finalOrderID := paymentTypeResp.orderID - - // 查找订单并更新状态为已支付 order, findOrderErr := l.svcCtx.OrderModel.FindOne(context.Background(), finalOrderID) if findOrderErr != nil { logx.Errorf("开发测试模式,查找订单失败,订单ID: %d, 错误: %v", finalOrderID, findOrderErr) return } - - // 更新订单状态为已支付 order.Status = "paid" now := time.Now() order.PayTime = sql.NullTime{Time: now, Valid: true} - // 更新订单 + // 空报告模式:在 PaymentPlatform 标记为 "test",在 paySuccessNotify.ProcessTask 中通过 + // order.PaymentPlatform == "test" 识别(isEmptyReportMode),并生成空报告、跳过 API 调用 + isEmptyReportMode := req.PayMethod == "test" + if isEmptyReportMode { + order.PaymentPlatform = "test" + logx.Infof("开发环境空报告模式:订单 %s (ID: %d) 已标记为空报告模式", paymentTypeResp.outTradeNo, finalOrderID) + } updateErr := l.svcCtx.OrderModel.UpdateWithVersion(context.Background(), nil, order) if updateErr != nil { logx.Errorf("开发测试模式,更新订单状态失败,订单ID: %d, 错误: %v", finalOrderID, updateErr) return } - logx.Infof("开发测试模式,订单状态已更新为已支付,订单ID: %d", finalOrderID) + if enqErr := l.svcCtx.AsynqService.SendQueryTask(finalOrderID); enqErr != nil { + logx.Errorf("开发测试模式,入队生成报告失败,订单ID: %d, 错误: %v", finalOrderID, enqErr) + } + + logx.Infof("开发测试模式,订单状态已更新为已支付并已入队生成报告,订单ID: %d", finalOrderID) + + // 再次短暂延迟,确保订单状态更新已提交 + time.Sleep(100 * time.Millisecond) }() } @@ -162,6 +188,42 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses if user.Inside == 1 { amount = 0.01 } + + // 检查72小时内身份证查询次数限制 + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取AES密钥失败: %+v", decodeErr) + } + // 解密缓存中的参数 + decryptedParams, decryptErr := crypto.AesDecrypt(data.Params, key) + if decryptErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 解密缓存参数失败: %v", decryptErr) + } + var params map[string]interface{} + if unmarshalErr := json.Unmarshal(decryptedParams, ¶ms); unmarshalErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 解析解密参数失败: %v", unmarshalErr) + } + // 获取身份证号 + idCard, ok := params["id_card"].(string) + if !ok || idCard == "" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取身份证号失败") + } + // 加密身份证号用于查询 + encryptedIdCard, encryptErr := crypto.EncryptIDCard(idCard, key) + if encryptErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 加密身份证号失败: %v", encryptErr) + } + // 查询72小时内的查询次数 + queryCount, countErr := l.svcCtx.QueryUserRecordModel.CountByEncryptedIdCardIn72Hours(l.ctx, encryptedIdCard) + if countErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 查询记录失败: %v", countErr) + } + // 如果72小时内查询次数大于等于2次,禁止支付(当前这次是第3次) + if queryCount >= 2 { + return nil, errors.Wrapf(xerr.NewErrMsg("查询受限通知:检测到您72小时内已完成2次报告查询,系统已自动暂停服务。如需紧急查询,请联系客服申请临时额度。"), "生成订单, 查询次数超限: %d", queryCount) + } + var orderID int64 order := model.Order{ OrderNo: outTradeNo, @@ -182,6 +244,12 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses } orderID = insertedOrderID + // 更新查询用户记录表的 order_id,便于通过查询信息追溯订单 + if rec, findErr := l.svcCtx.QueryUserRecordModel.FindOneByQueryNo(l.ctx, outTradeNo); findErr == nil { + rec.OrderId = orderID + _, _ = l.svcCtx.QueryUserRecordModel.Update(l.ctx, session, rec) + } + if data.AgentIdentifier != "" { agent, parsingErr := l.agentParsing(data.AgentIdentifier) if parsingErr != nil { diff --git a/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go b/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go index 5d46f6b..cc521cf 100644 --- a/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go +++ b/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go @@ -90,6 +90,11 @@ func (l *WechatPayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, logx.Errorf("微信支付回调,更新订单失败%+v", updateErr) return nil } + // 更新查询用户记录表的 platform_order_id + if rec, findErr := l.svcCtx.QueryUserRecordModel.FindOneByQueryNo(l.ctx, *notification.OutTradeNo); findErr == nil { + rec.PlatformOrderId = lzUtils.StringToNullString(*notification.TransactionId) + _, _ = l.svcCtx.QueryUserRecordModel.Update(l.ctx, nil, rec) + } if order.Status == "paid" { if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil { diff --git a/app/main/api/internal/logic/query/queryservicelogic.go b/app/main/api/internal/logic/query/queryservicelogic.go index ce1d1d8..e466432 100644 --- a/app/main/api/internal/logic/query/queryservicelogic.go +++ b/app/main/api/internal/logic/query/queryservicelogic.go @@ -2,6 +2,7 @@ package query import ( "context" + "database/sql" "encoding/hex" "encoding/json" "fmt" @@ -107,6 +108,7 @@ func (l *QueryServiceLogic) ProcessMarriageLogic(req *types.QueryServiceReq) (*t if cacheDataErr != nil { return nil, cacheDataErr } + l.recordQueryUserRecord(params, "marriage", userID, cacheNo) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) @@ -167,7 +169,7 @@ func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq) if cacheDataErr != nil { return nil, cacheDataErr } - + l.recordQueryUserRecord(params, "homeservice", userID, cacheNo) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) @@ -228,7 +230,7 @@ func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceRe if cacheDataErr != nil { return nil, cacheDataErr } - + l.recordQueryUserRecord(params, "riskassessment", userID, cacheNo) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) @@ -288,7 +290,7 @@ func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq) if cacheDataErr != nil { return nil, cacheDataErr } - + l.recordQueryUserRecord(params, "companyinfo", userID, cacheNo) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) @@ -349,7 +351,7 @@ func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) ( if cacheDataErr != nil { return nil, cacheDataErr } - + l.recordQueryUserRecord(params, "rentalinfo", userID, cacheNo) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) @@ -410,7 +412,7 @@ func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryS if cacheDataErr != nil { return nil, cacheDataErr } - + l.recordQueryUserRecord(params, "preloanbackgroundcheck", userID, cacheNo) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) @@ -470,7 +472,7 @@ func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceR if cacheDataErr != nil { return nil, cacheDataErr } - + l.recordQueryUserRecord(params, "backgroundcheck", userID, cacheNo) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) @@ -528,7 +530,7 @@ func (l *QueryServiceLogic) ProcessPersonalDataLogic(req *types.QueryServiceReq) if cacheDataErr != nil { return nil, cacheDataErr } - + l.recordQueryUserRecord(params, "personalData", userID, cacheNo) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) @@ -559,16 +561,10 @@ func (l *QueryServiceLogic) DecryptData(data string) ([]byte, error) { // 校验验证码 func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error { - // 开发环境固定验证码为138888 - env := os.Getenv("ENV") - if env == "development" { - if code == "138888" { - logx.Infof("开发环境:验证码验证通过 %s", mobile) - return nil - } - return errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "开发环境验证码应为138888: %s", mobile) + // 开发环境下跳过验证码验证 + if os.Getenv("ENV") == "development" { + return nil } - secretKey := l.svcCtx.Config.Encrypt.SecretKey encryptedMobile, err := crypto.EncryptMobile(mobile, secretKey) if err != nil { @@ -590,6 +586,12 @@ func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error { // 二、三要素验证 func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error { + // 开发环境下跳过二/三要素验证(避免未授权IP调用天元API失败) + isDevelopment := os.Getenv("ENV") == "development" + if isDevelopment { + return nil + } + if !l.svcCtx.Config.SystemConfig.ThreeVerify { twoVerification := service.TwoFactorVerificationRequest{ Name: Name, @@ -654,6 +656,76 @@ func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product str return outTradeNo, nil } +// recordQueryUserRecord 写入查询用户记录表,用于通过姓名/身份证/手机号追溯订单 +// 重要:name、id_card、mobile 必须以 AES-ECB+Base64 密文入库,禁止写入明文 +func (l *QueryServiceLogic) recordQueryUserRecord(params map[string]interface{}, product string, userID int64, queryNo string) { + getStr := func(k string) string { + if v, ok := params[k]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" + } + secretKey := l.svcCtx.Config.Encrypt.SecretKey + if secretKey == "" { + l.Errorf("查询用户记录表加密失败, Encrypt.SecretKey 未配置,拒绝写入明文 queryNo=%s", queryNo) + return + } + key, keyErr := hex.DecodeString(secretKey) + if keyErr != nil { + l.Errorf("查询用户记录表加密失败, 密钥解析错误 queryNo=%s err=%v", queryNo, keyErr) + return + } + // 以下三字段仅使用加密后的值赋值,不得使用 getStr 的明文 + encName := "" + if name := getStr("name"); name != "" { + if s, err := crypto.AesEcbEncrypt([]byte(name), key); err != nil { + l.Errorf("查询用户记录表姓名加密失败 queryNo=%s err=%v", queryNo, err) + return + } else { + encName = s + } + } + encIdCard := "" + if idCard := getStr("id_card"); idCard != "" { + if s, err := crypto.EncryptIDCard(idCard, key); err != nil { + l.Errorf("查询用户记录表身份证加密失败 queryNo=%s err=%v", queryNo, err) + return + } else { + encIdCard = s + } + } + encMobile := "" + if mobile := getStr("mobile"); mobile != "" { + if s, err := crypto.EncryptMobile(mobile, secretKey); err != nil { + l.Errorf("查询用户记录表手机号加密失败 queryNo=%s err=%v", queryNo, err) + return + } else { + encMobile = s + } + } + agentIdentifier := sql.NullString{} + if v, ok := l.ctx.Value("agentIdentifier").(string); ok && v != "" { + agentIdentifier = sql.NullString{String: v, Valid: true} + } + // rec 的 Name、IdCard、Mobile 仅使用密文 encName、encIdCard、encMobile + rec := &model.QueryUserRecord{ + UserId: userID, + Name: encName, + IdCard: encIdCard, + Mobile: encMobile, + Product: product, + QueryNo: queryNo, + OrderId: 0, + PlatformOrderId: sql.NullString{}, + AgentIdentifier: agentIdentifier, + } + if _, err := l.svcCtx.QueryUserRecordModel.Insert(l.ctx, nil, rec); err != nil { + l.Errorf("查询用户记录表写入失败 queryNo=%s err=%v", queryNo, err) + } +} + // GetOrCreateUser 获取或创建用户 // 1. 如果上下文中已有用户ID,直接返回 // 2. 如果是代理查询或APP请求,创建新用户 diff --git a/app/main/api/internal/logic/user/bindmobilelogic.go b/app/main/api/internal/logic/user/bindmobilelogic.go index b609ea2..2d62599 100644 --- a/app/main/api/internal/logic/user/bindmobilelogic.go +++ b/app/main/api/internal/logic/user/bindmobilelogic.go @@ -43,14 +43,8 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定手机号, 加密手机号失败: %v", err) } - // 开发环境固定验证码为138888 - env := os.Getenv("ENV") - if env == "development" { - if req.Code != "138888" { - return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "开发环境验证码应为138888") - } - logx.Infof("开发环境:验证码验证通过 %s", req.Mobile) - } else if req.Mobile != "18889793585" { + // 开发环境下跳过验证码验证,或者特定手机号跳过(保留原有逻辑) + if os.Getenv("ENV") != "development" && req.Mobile != "18889793585" { // 检查手机号是否在一分钟内已发送过验证码 redisKey := fmt.Sprintf("%s:%s", "bindMobile", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) diff --git a/app/main/api/internal/logic/user/mobilecodeloginlogic.go b/app/main/api/internal/logic/user/mobilecodeloginlogic.go index fd27947..2775349 100644 --- a/app/main/api/internal/logic/user/mobilecodeloginlogic.go +++ b/app/main/api/internal/logic/user/mobilecodeloginlogic.go @@ -38,15 +38,8 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 加密手机号失败: %+v", err) } - - // 开发环境固定验证码为138888 - env := os.Getenv("ENV") - if env == "development" { - if req.Code != "138888" { - return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "开发环境验证码应为138888") - } - logx.Infof("开发环境:验证码验证通过 %s", req.Mobile) - } else { + // 开发环境下跳过验证码验证 + if os.Getenv("ENV") != "development" { // 检查手机号是否在一分钟内已发送过验证码 redisKey := fmt.Sprintf("%s:%s", "login", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) diff --git a/app/main/api/internal/queue/paySuccessNotify.go b/app/main/api/internal/queue/paySuccessNotify.go index f0fca2f..9a3d441 100644 --- a/app/main/api/internal/queue/paySuccessNotify.go +++ b/app/main/api/internal/queue/paySuccessNotify.go @@ -11,6 +11,7 @@ import ( "regexp" "strings" paylogic "tydata-server/app/main/api/internal/logic/pay" + "tydata-server/app/main/api/internal/service" "tydata-server/app/main/api/internal/svc" "tydata-server/app/main/api/internal/types" "tydata-server/app/main/model" @@ -45,12 +46,13 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq. if err != nil { return fmt.Errorf("无效的订单ID: %d, %v", payload.OrderID, err) } - env := os.Getenv("ENV") - if order.Status != "paid" && env != "development" { - err = fmt.Errorf("无效的订单: %d", payload.OrderID) + // 必须已支付才处理:仅支付宝/微信/苹果回调或 pay_method=test 的异步流程会将订单标为 paid,此处不再按 ENV 放宽 + if order.Status != "paid" { + err = fmt.Errorf("无效的订单状态(非已支付): orderID=%d, status=%s", payload.OrderID, order.Status) logx.Errorf("处理任务失败,原因: %v", err) return asynq.SkipRetry } + env := os.Getenv("ENV") product, err := l.svcCtx.ProductModel.FindOne(ctx, order.ProductId) if err != nil { return fmt.Errorf("找不到相关产品: orderID: %d, productID: %d", payload.OrderID, order.ProductId) @@ -141,11 +143,20 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq. decryptData = updatedDecryptData } } - - // 调用API请求服务 - responseData, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id) - if err != nil { - return l.handleError(ctx, err, order, query) + // 调用API请求服务(开发环境下不调用其它产品,使用默认空报告) + var responseData []service.APIResponseData + if env == "development" { + // 开发环境:生成仅包含基本信息的默认空报告,不调用外部 API + // 空报告模式:生成空的报告数据,跳过API调用 + logx.Infof("空报告模式:订单 %s (ID: %s) 跳过API调用,生成空报告", order.OrderNo, order.Id) + // 空数组,表示没有数据;与 json.Marshal 配合得到 [] + responseData = []service.APIResponseData{} + } else { + var processErr error + responseData, processErr = l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id) + if processErr != nil { + return l.handleError(ctx, processErr, order, query) + } } // 计算成功模块的总成本价 @@ -287,6 +298,7 @@ func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error return asynq.SkipRetry } + // desensitizeParams 对敏感数据进行脱敏处理 func (l *PaySuccessNotifyUserHandler) desensitizeParams(data []byte) ([]byte, error) { // 解析JSON数据到map diff --git a/app/main/api/internal/service/apirequestService.go b/app/main/api/internal/service/apirequestService.go index 1387540..37b1702 100644 --- a/app/main/api/internal/service/apirequestService.go +++ b/app/main/api/internal/service/apirequestService.go @@ -1434,6 +1434,7 @@ func (a *ApiRequestService) ProcessIVYZ3P9MRequest(params []byte) ([]byte, error return convertTianyuanResponse(resp) } + // ProcessFLXG7E8FRequest 个人涉诉 func (a *ApiRequestService) ProcessFLXG7E8FRequest(params []byte) ([]byte, error) { idCard := gjson.GetBytes(params, "id_card") @@ -1444,9 +1445,9 @@ func (a *ApiRequestService) ProcessFLXG7E8FRequest(params []byte) ([]byte, error } resp, err := a.tianyuanapi.CallInterface("FLXG7E8F", map[string]interface{}{ - "id_card": idCard.String(), - "name": name.String(), - "mobile_no": mobile.String(), + "id_card": idCard.String(), + "name": name.String(), + "mobile_no": mobile.String(), }) if err != nil { @@ -1454,4 +1455,4 @@ func (a *ApiRequestService) ProcessFLXG7E8FRequest(params []byte) ([]byte, error } return convertTianyuanResponse(resp) -} \ No newline at end of file +} diff --git a/app/main/api/internal/svc/servicecontext.go b/app/main/api/internal/svc/servicecontext.go index 4d646f8..9881c9e 100644 --- a/app/main/api/internal/svc/servicecontext.go +++ b/app/main/api/internal/svc/servicecontext.go @@ -39,6 +39,7 @@ type ServiceContext struct { OrderModel model.OrderModel OrderRefundModel model.OrderRefundModel QueryModel model.QueryModel + QueryUserRecordModel model.QueryUserRecordModel QueryCleanupLogModel model.QueryCleanupLogModel QueryCleanupDetailModel model.QueryCleanupDetailModel QueryCleanupConfigModel model.QueryCleanupConfigModel @@ -108,10 +109,12 @@ func NewServiceContext(c config.Config) *ServiceContext { cacheConf := c.CacheRedis // 初始化Redis客户端 + // 设置超时时间为30秒,解决连接超时问题 redisConf := redis.RedisConf{ - Host: cacheConf[0].Host, - Pass: cacheConf[0].Pass, - Type: cacheConf[0].Type, + Host: cacheConf[0].Host, + Pass: cacheConf[0].Pass, + Type: cacheConf[0].Type, + PingTimeout: 30 * time.Second, // 设置30秒超时,解决 "context deadline exceeded" 错误 } redisClient := redis.MustNewRedis(redisConf) @@ -128,6 +131,7 @@ func NewServiceContext(c config.Config) *ServiceContext { // ============================== 订单相关模型 ============================== orderModel := model.NewOrderModel(db, cacheConf) queryModel := model.NewQueryModel(db, cacheConf) + queryUserRecordModel := model.NewQueryUserRecordModel(db, cacheConf) orderRefundModel := model.NewOrderRefundModel(db, cacheConf) queryCleanupLogModel := model.NewQueryCleanupLogModel(db, cacheConf) queryCleanupDetailModel := model.NewQueryCleanupDetailModel(db, cacheConf) @@ -238,6 +242,7 @@ func NewServiceContext(c config.Config) *ServiceContext { // 订单相关模型 OrderModel: orderModel, QueryModel: queryModel, + QueryUserRecordModel: queryUserRecordModel, OrderRefundModel: orderRefundModel, QueryCleanupLogModel: queryCleanupLogModel, QueryCleanupDetailModel: queryCleanupDetailModel, diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index e1416b0..d7db89b 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -533,6 +533,9 @@ type AdminGetOrderListReq struct { RefundTimeStart string `form:"refund_time_start,optional"` // 退款时间开始 RefundTimeEnd string `form:"refund_time_end,optional"` // 退款时间结束 SalesCost float64 `form:"sales_cost,optional"` // 成本价 + QueryName string `form:"query_name,optional"` // 被查询人姓名(通过 query_user_record 表追溯订单) + QueryIdCard string `form:"query_id_card,optional"` // 被查询人身份证(通过 query_user_record 表追溯订单) + QueryMobile string `form:"query_mobile,optional"` // 被查询人手机号(通过 query_user_record 表追溯订单) } type AdminGetOrderListResp struct { diff --git a/app/main/model/queryUserRecordModel.go b/app/main/model/queryUserRecordModel.go new file mode 100644 index 0000000..4c98580 --- /dev/null +++ b/app/main/model/queryUserRecordModel.go @@ -0,0 +1,76 @@ +package model + +import ( + "context" + "fmt" + "time" + + "tydata-server/common/globalkey" + + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ QueryUserRecordModel = (*customQueryUserRecordModel)(nil) + +type ( + // QueryUserRecordModel is an interface to be customized, add more methods here, + // and implement the added methods in customQueryUserRecordModel. + QueryUserRecordModel interface { + queryUserRecordModel + FindOneByQueryNo(ctx context.Context, queryNo string) (*QueryUserRecord, error) + CountByEncryptedIdCardIn72Hours(ctx context.Context, encryptedIdCard string) (int64, error) + } + + customQueryUserRecordModel struct { + *defaultQueryUserRecordModel + } +) + +// FindOneByQueryNo 根据 query_no 查询一条记录(query_no 与 order.order_no 一致) +func (m *customQueryUserRecordModel) FindOneByQueryNo(ctx context.Context, queryNo string) (*QueryUserRecord, error) { + query := fmt.Sprintf("select %s from %s where `query_no` = ? and del_state = ? limit 1", queryUserRecordRows, m.table) + var resp QueryUserRecord + err := m.QueryRowNoCacheCtx(ctx, &resp, query, queryNo, globalkey.DelStateNo) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// CountByEncryptedIdCardIn72Hours 查询72小时内某个加密身份证号的已支付查询次数 +func (m *customQueryUserRecordModel) CountByEncryptedIdCardIn72Hours(ctx context.Context, encryptedIdCard string) (int64, error) { + // 计算72小时前的时间 + seventyTwoHoursAgo := time.Now().Add(-72 * time.Hour) + + // 关联 order 表,只统计已支付的订单 + query := fmt.Sprintf(` + select count(*) + from %s qur + inner join `+"`order`"+` o on qur.order_id = o.id + where qur.id_card = ? + and qur.create_time >= ? + and qur.del_state = ? + and qur.order_id > 0 + and o.status = 'paid' + and o.del_state = ? + `, m.table) + var count int64 + err := m.QueryRowNoCacheCtx(ctx, &count, query, encryptedIdCard, seventyTwoHoursAgo, globalkey.DelStateNo, globalkey.DelStateNo) + if err != nil { + return 0, err + } + return count, nil +} + +// NewQueryUserRecordModel returns a model for the database table. +func NewQueryUserRecordModel(conn sqlx.SqlConn, c cache.CacheConf) QueryUserRecordModel { + return &customQueryUserRecordModel{ + defaultQueryUserRecordModel: newQueryUserRecordModel(conn, c), + } +} diff --git a/app/main/model/queryUserRecordModel_gen.go b/app/main/model/queryUserRecordModel_gen.go new file mode 100644 index 0000000..0191672 --- /dev/null +++ b/app/main/model/queryUserRecordModel_gen.go @@ -0,0 +1,376 @@ +// Code generated by goctl. DO NOT EDIT! + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "time" + + "tydata-server/common/globalkey" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + queryUserRecordFieldNames = builder.RawFieldNames(&QueryUserRecord{}) + queryUserRecordRows = strings.Join(queryUserRecordFieldNames, ",") + queryUserRecordRowsExpectAutoSet = strings.Join(stringx.Remove(queryUserRecordFieldNames, "`id`", "`create_time`", "`update_time`"), ",") + queryUserRecordRowsWithPlaceHolder = strings.Join(stringx.Remove(queryUserRecordFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" + + cacheTydataQueryUserRecordIdPrefix = "cache:tydata:queryUserRecord:id:" +) + +type ( + queryUserRecordModel interface { + Insert(ctx context.Context, session sqlx.Session, data *QueryUserRecord) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*QueryUserRecord, error) + Update(ctx context.Context, session sqlx.Session, data *QueryUserRecord) (sql.Result, error) + UpdateWithVersion(ctx context.Context, session sqlx.Session, data *QueryUserRecord) error + Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error + SelectBuilder() squirrel.SelectBuilder + DeleteSoft(ctx context.Context, session sqlx.Session, data *QueryUserRecord) error + FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) + FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) + FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*QueryUserRecord, error) + FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*QueryUserRecord, error) + FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*QueryUserRecord, int64, error) + FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*QueryUserRecord, error) + FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*QueryUserRecord, error) + Delete(ctx context.Context, session sqlx.Session, id int64) error + } + + defaultQueryUserRecordModel struct { + sqlc.CachedConn + table string + } + + QueryUserRecord struct { + Id int64 `db:"id"` + CreateTime time.Time `db:"create_time"` + UpdateTime time.Time `db:"update_time"` + DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 + DelState int64 `db:"del_state"` + Version int64 `db:"version"` // 版本号 + UserId int64 `db:"user_id"` // 用户ID + Name string `db:"name"` // 姓名密文(AES-ECB+Base64) + IdCard string `db:"id_card"` // 身份证号密文(AES-ECB+Base64) + Mobile string `db:"mobile"` // 手机号密文(AES-ECB+Base64) + Product string `db:"product"` // 产品类型,如 marriage/homeservice/riskassessment 等 + QueryNo string `db:"query_no"` // 查询单号(与 order.order_no 一致,如 Q_xxx),用户提交查询时生成 + OrderId int64 `db:"order_id"` // 订单ID,关联 order 表,用户发起支付并创建订单后写入 + PlatformOrderId sql.NullString `db:"platform_order_id"` // 支付平台订单号(支付宝/微信),支付成功后由回调写入 + AgentIdentifier sql.NullString `db:"agent_identifier"` // 代理标识,代理渠道时有值 + } +) + +func newQueryUserRecordModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultQueryUserRecordModel { + return &defaultQueryUserRecordModel{ + CachedConn: sqlc.NewConn(conn, c), + table: "`query_user_record`", + } +} + +func (m *defaultQueryUserRecordModel) Insert(ctx context.Context, session sqlx.Session, data *QueryUserRecord) (sql.Result, error) { + data.DelState = globalkey.DelStateNo + tydataQueryUserRecordIdKey := fmt.Sprintf("%s%v", cacheTydataQueryUserRecordIdPrefix, data.Id) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, queryUserRecordRowsExpectAutoSet) + if session != nil { + return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.UserId, data.Name, data.IdCard, data.Mobile, data.Product, data.QueryNo, data.OrderId, data.PlatformOrderId, data.AgentIdentifier) + } + return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.UserId, data.Name, data.IdCard, data.Mobile, data.Product, data.QueryNo, data.OrderId, data.PlatformOrderId, data.AgentIdentifier) + }, tydataQueryUserRecordIdKey) +} + +func (m *defaultQueryUserRecordModel) FindOne(ctx context.Context, id int64) (*QueryUserRecord, error) { + tydataQueryUserRecordIdKey := fmt.Sprintf("%s%v", cacheTydataQueryUserRecordIdPrefix, id) + var resp QueryUserRecord + err := m.QueryRowCtx(ctx, &resp, tydataQueryUserRecordIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", queryUserRecordRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultQueryUserRecordModel) Update(ctx context.Context, session sqlx.Session, data *QueryUserRecord) (sql.Result, error) { + tydataQueryUserRecordIdKey := fmt.Sprintf("%s%v", cacheTydataQueryUserRecordIdPrefix, data.Id) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, queryUserRecordRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.UserId, data.Name, data.IdCard, data.Mobile, data.Product, data.QueryNo, data.OrderId, data.PlatformOrderId, data.AgentIdentifier, data.Id) + } + return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.UserId, data.Name, data.IdCard, data.Mobile, data.Product, data.QueryNo, data.OrderId, data.PlatformOrderId, data.AgentIdentifier, data.Id) + }, tydataQueryUserRecordIdKey) +} + +func (m *defaultQueryUserRecordModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, data *QueryUserRecord) error { + + oldVersion := data.Version + data.Version += 1 + + var sqlResult sql.Result + var err error + + tydataQueryUserRecordIdKey := fmt.Sprintf("%s%v", cacheTydataQueryUserRecordIdPrefix, data.Id) + sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, queryUserRecordRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.UserId, data.Name, data.IdCard, data.Mobile, data.Product, data.QueryNo, data.OrderId, data.PlatformOrderId, data.AgentIdentifier, data.Id, oldVersion) + } + return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.UserId, data.Name, data.IdCard, data.Mobile, data.Product, data.QueryNo, data.OrderId, data.PlatformOrderId, data.AgentIdentifier, data.Id, oldVersion) + }, tydataQueryUserRecordIdKey) + if err != nil { + return err + } + updateCount, err := sqlResult.RowsAffected() + if err != nil { + return err + } + if updateCount == 0 { + return ErrNoRowsUpdate + } + + return nil +} + +func (m *defaultQueryUserRecordModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *QueryUserRecord) error { + data.DelState = globalkey.DelStateYes + data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} + if err := m.UpdateWithVersion(ctx, session, data); err != nil { + return errors.Wrapf(errors.New("delete soft failed "), "QueryUserRecordModel delete err : %+v", err) + } + return nil +} + +func (m *defaultQueryUserRecordModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") + } + + builder = builder.Columns("IFNULL(SUM(" + field + "),0)") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp float64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultQueryUserRecordModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") + } + + builder = builder.Columns("COUNT(" + field + ")") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp int64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultQueryUserRecordModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*QueryUserRecord, error) { + + builder = builder.Columns(queryUserRecordRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return nil, err + } + + var resp []*QueryUserRecord + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultQueryUserRecordModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*QueryUserRecord, error) { + + builder = builder.Columns(queryUserRecordRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*QueryUserRecord + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultQueryUserRecordModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*QueryUserRecord, int64, error) { + + total, err := m.FindCount(ctx, builder, "id") + if err != nil { + return nil, 0, err + } + + builder = builder.Columns(queryUserRecordRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, total, err + } + + var resp []*QueryUserRecord + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, total, nil + default: + return nil, total, err + } +} + +func (m *defaultQueryUserRecordModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*QueryUserRecord, error) { + + builder = builder.Columns(queryUserRecordRows) + + if preMinId > 0 { + builder = builder.Where(" id < ? ", preMinId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*QueryUserRecord + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultQueryUserRecordModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*QueryUserRecord, error) { + + builder = builder.Columns(queryUserRecordRows) + + if preMaxId > 0 { + builder = builder.Where(" id > ? ", preMaxId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*QueryUserRecord + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultQueryUserRecordModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { + + return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { + return fn(ctx, session) + }) + +} + +func (m *defaultQueryUserRecordModel) SelectBuilder() squirrel.SelectBuilder { + return squirrel.Select().From(m.table) +} +func (m *defaultQueryUserRecordModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { + tydataQueryUserRecordIdKey := fmt.Sprintf("%s%v", cacheTydataQueryUserRecordIdPrefix, id) + _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + if session != nil { + return session.ExecCtx(ctx, query, id) + } + return conn.ExecCtx(ctx, query, id) + }, tydataQueryUserRecordIdKey) + return err +} +func (m *defaultQueryUserRecordModel) formatPrimary(primary interface{}) string { + return fmt.Sprintf("%s%v", cacheTydataQueryUserRecordIdPrefix, primary) +} +func (m *defaultQueryUserRecordModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", queryUserRecordRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) +} + +func (m *defaultQueryUserRecordModel) tableName() string { + return m.table +} diff --git a/deploy/script/gen_models.ps1 b/deploy/script/gen_models.ps1 index c2cb08c..54d1e1a 100644 --- a/deploy/script/gen_models.ps1 +++ b/deploy/script/gen_models.ps1 @@ -41,6 +41,7 @@ $tables = @( # "user_auth" # "user_temp" # "example" + # "query_user_record" # 查询用户记录表:姓名、身份证、手机号、支付订单号等 # "admin_user" # "admin_user_role" # "admin_api", diff --git a/deploy/sql/query_user_record.sql b/deploy/sql/query_user_record.sql new file mode 100644 index 0000000..edf1278 --- /dev/null +++ b/deploy/sql/query_user_record.sql @@ -0,0 +1,42 @@ +-- 查询用户记录表 +-- 用途:记录用户查询时输入的姓名、身份证、手机号,以及支付订单号等,用于通过查询条件追溯订单信息 +-- 执行说明:在目标数据库执行此脚本创建表 +-- +-- 使用说明(需在业务代码中接入): +-- 1. 用户提交查询时(queryservicelogic.CacheData 之后):INSERT 记录 name, id_card, mobile(已 AES-ECB+Base64 加密), product, query_no, user_id, agent_identifier +-- 2. 用户发起支付并创建 order 时(paymentlogic.QueryOrderPayment 中 Insert order 之后):UPDATE 本表 SET order_id WHERE query_no=outTradeNo +-- 3. 支付回调成功更新 order 后(alipaycallbacklogic/wechatpaycallbacklogic):UPDATE 本表 SET platform_order_id WHERE query_no=orderNo +-- +-- 敏感字段加密:name、id_card、mobile 使用 pkg/lzkit/crypto 的 AES-ECB+Base64 加密后入库,密钥为 config.Encrypt.SecretKey(hex)。 +-- 解密:姓名 crypto.AesEcbDecrypt(rec.Name, key);身份证 crypto.DecryptIDCard(rec.IdCard, key);手机 crypto.DecryptMobile(rec.Mobile, secretKey)。 + +CREATE TABLE `query_user_record` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `del_state` tinyint NOT NULL DEFAULT '0', + `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号', + + /* 业务字段 - 用户查询输入(name、id_card、mobile 为 AES-ECB+Base64 密文) */ + `user_id` bigint NOT NULL DEFAULT '0' COMMENT '用户ID', + `name` varchar(256) NOT NULL DEFAULT '' COMMENT '姓名密文(AES-ECB+Base64)', + `id_card` varchar(128) NOT NULL DEFAULT '' COMMENT '身份证号密文(AES-ECB+Base64)', + `mobile` varchar(128) NOT NULL DEFAULT '' COMMENT '手机号密文(AES-ECB+Base64)', + `product` varchar(50) NOT NULL DEFAULT '' COMMENT '产品类型,如 marriage/homeservice/riskassessment 等', + + /* 关联字段 - 查询单号与订单 */ + `query_no` varchar(64) NOT NULL DEFAULT '' COMMENT '查询单号(与 order.order_no 一致,如 Q_xxx),用户提交查询时生成', + `order_id` bigint NOT NULL DEFAULT '0' COMMENT '订单ID,关联 order 表,用户发起支付并创建订单后写入', + `platform_order_id` varchar(64) DEFAULT NULL COMMENT '支付平台订单号(支付宝/微信),支付成功后由回调写入', + `agent_identifier` varchar(255) DEFAULT NULL COMMENT '代理标识,代理渠道时有值', + + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_id_card` (`id_card`), + KEY `idx_mobile` (`mobile`), + KEY `idx_query_no` (`query_no`), + KEY `idx_order_id` (`order_id`), + KEY `idx_platform_order_id` (`platform_order_id`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='查询用户记录表:姓名、身份证、手机号、支付订单号等,用于通过查询信息追溯订单'; diff --git a/deploy/sql/template.sql b/deploy/sql/template.sql deleted file mode 100644 index 1e732f4..0000000 --- a/deploy/sql/template.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TABLE `表名` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - `del_state` tinyint NOT NULL DEFAULT '0', - `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号', - - /* 业务字段开始 */ - `字段1` 数据类型 [约束条件] [DEFAULT 默认值] [COMMENT '字段说明'], - `字段2` 数据类型 [约束条件] [DEFAULT 默认值] [COMMENT '字段说明'], - /* 关联字段 - 软关联 */ - `关联表id` bigint [NOT NULL] [DEFAULT '0'] COMMENT '关联到XX表的id', - /* 业务字段结束 */ - - PRIMARY KEY (`id`), - /* 索引定义 */ - UNIQUE KEY `索引名称` (`字段名`), - KEY `idx_关联字段` (`关联表id`) COMMENT '优化关联查询' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='表说明'; \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 500e54c..d7e6368 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,7 +13,7 @@ services: MYSQL_USER: znc MYSQL_PASSWORD: 5vg67b3UNHu823 ports: - - "21001:3306" + - "21101:3306" volumes: # 数据挂载 - Data mounting - ./data/mysql/data:/var/lib/mysql @@ -35,7 +35,7 @@ services: image: redis:7.4.0 container_name: znc_redis ports: - - "21002:6379" + - "21102:6379" environment: # 时区上海 - Time zone Shanghai (Change if needed) TZ: Asia/Shanghai @@ -52,7 +52,7 @@ services: image: hibiken/asynqmon:latest container_name: znc_asynqmon ports: - - "21003:8080" + - "21103:8080" environment: - TZ=Asia/Shanghai command: @@ -75,7 +75,7 @@ services: - PMA_USER=znc - PMA_PASSWORD=5vg67b3UNHu823 ports: - - "21006:80" + - "21106:80" depends_on: - mysql networks: @@ -87,13 +87,13 @@ services: context: . dockerfile: app/main/api/Dockerfile ports: - - "21005:8888" + - "21105:8888" volumes: - ./data/authorization_docs:/app/data/authorization_docs:rw environment: - TZ=Asia/Shanghai - ENV=development - command: sh -c "until nc -z znc_redis 6379; do echo '等待 Redis...'; sleep 1; done && echo 'Redis 端口已开放,等待完全就绪...' && sleep 5 && echo '启动应用' && ./main" + command: sh -c "echo '等待 Redis 启动...' && until nc -z znc_redis 6379; do echo '等待 Redis 端口开放...'; sleep 1; done && echo 'Redis 端口已开放,等待服务完全就绪...' && sleep 10 && echo '启动应用' && ./main" depends_on: - mysql - redis